devflow-kit 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +44 -19
- package/dist/agents/git.md +13 -15
- package/dist/cli/commands/ambient.js +160 -145
- package/dist/cli/commands/capture.js +29 -55
- package/dist/cli/commands/compliance.js +32 -61
- package/dist/cli/commands/context.js +17 -32
- package/dist/cli/commands/debug.js +65 -26
- package/dist/cli/commands/flags.js +3 -3
- package/dist/cli/commands/hud.js +34 -10
- package/dist/cli/commands/init-seed.js +40 -4
- package/dist/cli/commands/init.js +249 -271
- package/dist/cli/commands/install-report.js +10 -15
- package/dist/cli/commands/knowledge/index.js +1 -1
- package/dist/cli/commands/knowledge/toggle.js +11 -3
- package/dist/cli/commands/learning.js +52 -37
- package/dist/cli/commands/legacy-hooks.js +11 -14
- package/dist/cli/commands/memory.js +67 -78
- package/dist/cli/commands/proxy.js +23 -41
- package/dist/cli/commands/security.js +5 -13
- package/dist/cli/commands/skills.js +21 -3
- package/dist/cli/commands/tracker.js +100 -228
- package/dist/cli/commands/uninstall.js +343 -138
- package/dist/commands/bug-analysis.md +38 -12
- package/dist/commands/code-review.md +70 -21
- package/dist/commands/debug.md +37 -7
- package/dist/commands/dynamic-build.md +66 -17
- package/dist/commands/dynamic-plan.md +19 -8
- package/dist/commands/dynamic-profile.md +24 -10
- package/dist/commands/dynamic-tickets.md +22 -11
- package/dist/commands/explore.md +37 -7
- package/dist/commands/implement.md +96 -32
- package/dist/commands/plan.md +62 -19
- package/dist/commands/release.md +2 -2
- package/dist/commands/research.md +34 -8
- package/dist/commands/resolve.md +65 -17
- package/dist/commands/self-review.md +45 -9
- package/dist/core/compliance-compose.js +27 -27
- package/dist/core/evidence-policy.js +240 -24
- package/dist/core/feature-config.js +94 -25
- package/dist/core/feature-switch.js +1 -1
- package/dist/core/flags.js +30 -2
- package/dist/core/fs-atomic.js +27 -0
- package/dist/core/hook-log-dirs.js +104 -0
- package/dist/core/learning-tuning-config.js +5 -3
- package/dist/core/ledger-root.js +102 -0
- package/dist/core/manifest.js +6 -4
- package/dist/core/mds-variants.js +34 -97
- package/dist/core/migrations.js +49 -23
- package/dist/core/plugins.js +5 -4
- package/dist/core/project-paths.js +0 -17
- package/dist/core/same-location.js +25 -0
- package/dist/core/tracker.js +226 -139
- package/dist/hud/components/config-counts.js +15 -4
- package/dist/hud/components/learning-counts.js +14 -0
- package/dist/hud/config.js +2 -1
- package/dist/hud/cost-history.js +2 -4
- package/dist/hud/git.js +52 -7
- package/dist/hud/index.js +7 -9
- package/dist/skills/git/references/pr/check-merge-readiness.md +1 -1
- package/dist/skills/git/references/pr/ensure-pr-ready.md +1 -1
- package/dist/skills/git/references/pr/update-pr-evidence.md +1 -1
- package/dist/skills/git/references/tracker/_mcp.md +1 -1
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/github/manage-debt.md +3 -3
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/jira/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/jira/setup-task.md +1 -1
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +1 -1
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +1 -1
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +2 -2
- package/dist/skills/git/references/tracker/linear/manage-debt.md +1 -1
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +1 -1
- package/dist/skills/git/references/tracker/linear/setup-task.md +1 -1
- package/dist/targets/claude-code/claude-paths.js +59 -57
- package/dist/targets/claude-code/compliance-install.js +49 -65
- package/dist/targets/claude-code/hooks.js +108 -3
- package/dist/targets/claude-code/installer.js +30 -57
- package/dist/targets/claude-code/post-install.js +246 -139
- package/dist/targets/claude-code/tracker-install.js +38 -65
- package/package.json +5 -4
- package/src/assets/agents/code.md +4 -3
- package/src/assets/agents/design.md +1 -0
- package/src/assets/agents/git.mds +55 -57
- package/src/assets/agents/knowledge.md +2 -2
- package/src/assets/agents/review.md +3 -1
- package/src/assets/agents/tracker.md +37 -30
- package/src/assets/commands/_partials/_compliance.mds +19 -1
- package/src/assets/commands/_partials/_decisions.mds +15 -3
- package/src/assets/commands/_partials/_docs_root.mds +35 -0
- package/src/assets/commands/_partials/_engine.mds +2 -2
- package/src/assets/commands/_partials/_evidence_policy.mds +3 -3
- package/src/assets/commands/_partials/_factory.mds +1 -1
- package/src/assets/commands/_partials/_knowledge.mds +27 -9
- package/src/assets/commands/_partials/_plan_contract.mds +2 -2
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +28 -0
- package/src/assets/commands/_partials/_ticket_template.mds +3 -3
- package/src/assets/commands/_partials/_tracker.mds +4 -4
- package/src/assets/commands/_partials/_wave.mds +4 -4
- package/src/assets/commands/bug-analysis.mds +19 -17
- package/src/assets/commands/code-review.mds +39 -33
- package/src/assets/commands/debug.mds +4 -5
- package/src/assets/commands/dynamic-build.mds +75 -53
- package/src/assets/commands/dynamic-plan.mds +20 -15
- package/src/assets/commands/dynamic-profile.mds +24 -11
- package/src/assets/commands/dynamic-tickets.mds +25 -20
- package/src/assets/commands/explore.mds +4 -5
- package/src/assets/commands/implement.mds +58 -45
- package/src/assets/commands/plan.mds +34 -29
- package/src/assets/commands/release.md +2 -2
- package/src/assets/commands/research.mds +11 -9
- package/src/assets/commands/resolve.mds +41 -39
- package/src/assets/commands/self-review.mds +24 -25
- package/src/assets/mds/git/_pr.mds +61 -61
- package/src/assets/mds/git/_references.mds +19 -19
- package/src/assets/mds/tracker/_common.mds +8 -8
- package/src/assets/mds/tracker/_github.mds +71 -71
- package/src/assets/mds/tracker/_jira.mds +74 -74
- package/src/assets/mds/tracker/_linear.mds +75 -75
- package/src/assets/mds/tracker/_mcp.mds +23 -17
- package/src/assets/scripts/hooks/background-memory-update +35 -19
- package/src/assets/scripts/hooks/capture-prompt +18 -12
- package/src/assets/scripts/hooks/capture-question +18 -12
- package/src/assets/scripts/hooks/capture-turn +27 -17
- package/src/assets/scripts/hooks/debug-trace +11 -6
- package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
- package/src/assets/scripts/hooks/ensure-proxy +9 -8
- package/src/assets/scripts/hooks/ensure-root-gitignore +111 -36
- package/src/assets/scripts/hooks/git-marker +48 -0
- package/src/assets/scripts/hooks/json-helper.cjs +6 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +0 -19
- package/src/assets/scripts/hooks/log-paths +80 -0
- package/src/assets/scripts/hooks/memory-worker +17 -15
- package/src/assets/scripts/hooks/pre-compact-memory +41 -16
- package/src/assets/scripts/hooks/queue-append +104 -30
- package/src/assets/scripts/hooks/resolve-project-root +101 -7
- package/src/assets/scripts/hooks/session-start-context +289 -122
- package/src/assets/scripts/hooks/session-start-memory +35 -16
- package/src/assets/scripts/lib/project-config.cjs +633 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +300 -220
- package/src/assets/scripts/resolve-settings.cjs +1054 -0
- package/src/assets/scripts/verify-evidence.cjs +1 -1
- package/src/assets/skills/compliance/SKILL.md +2 -2
- package/src/assets/skills/docs-framework/SKILL.md +6 -7
- package/src/assets/skills/docs-framework/references/patterns.md +10 -17
- package/src/assets/skills/gap-analysis/SKILL.md +2 -2
- package/src/assets/skills/git/references/github-api.md +9 -9
- package/src/assets/skills/git/references/patterns.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +29 -0
- package/src/targets/claude-code/templates/managed-settings.json +25 -9
package/dist/core/tracker.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two halves, deliberately in one module:
|
|
5
5
|
* - The DOMAIN half (registry, TrackerProvider, parseTrackerId,
|
|
6
|
-
* normalizeTrackerFeature, path derivation
|
|
6
|
+
* normalizeTrackerFeature, path derivation, the frontmatter parser) is
|
|
7
|
+
* pure — zero I/O.
|
|
7
8
|
* - The LIFECYCLE half (rearmTrackerInference, applyTrackerSentinel,
|
|
8
|
-
*
|
|
9
|
+
* migrateLegacyTrackerConventions) owns the ~/.devflow tracker files.
|
|
9
10
|
* It sits here rather than in a target adapter because ~/.devflow is
|
|
10
11
|
* devflow-global, not Claude-Code-specific — the same reason manifest.ts's
|
|
11
12
|
* read/write live in src/core/ (applies ADR-013).
|
|
@@ -14,15 +15,16 @@
|
|
|
14
15
|
* fallible path returns a Result, so callers own their own error rendering
|
|
15
16
|
* and a try/finally in a caller is never skipped.
|
|
16
17
|
*
|
|
17
|
-
* D-TRACKER-OWNER [DR-22][DR-10]: the attempt
|
|
18
|
-
* have exactly ONE owner each — `rearmTrackerInference` and
|
|
18
|
+
* D-TRACKER-OWNER [DR-22][DR-10]: the attempt counters and the machine provider
|
|
19
|
+
* sentinel have exactly ONE owner each — `rearmTrackerInference` and
|
|
19
20
|
* `applyTrackerSentinel` — and every command that touches one goes through it.
|
|
20
21
|
* The sentinel is converged by `devflow init` and `devflow tracker --set`; the
|
|
21
|
-
*
|
|
22
|
-
* the command a capped user reaches for (D-F).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* inline an `fs.rm` at a call
|
|
22
|
+
* counters are re-armed by those two and by `devflow tracker --status`, which
|
|
23
|
+
* is the command a capped user reaches for (D-F). The conventions files are the
|
|
24
|
+
* Tracker agent's to write and the user's to edit: no command moves, renames or
|
|
25
|
+
* deletes one, and the single legacy file is moved once, by the migration that
|
|
26
|
+
* calls `migrateLegacyTrackerConventions`. Never inline an `fs.rm` at a call
|
|
27
|
+
* site.
|
|
26
28
|
*/
|
|
27
29
|
import { promises as fs } from 'fs';
|
|
28
30
|
import * as path from 'path';
|
|
@@ -57,42 +59,49 @@ export const TRACKER_PROVIDER_IDS = TRACKER_PROVIDERS.map(p => p.id);
|
|
|
57
59
|
* Existing installs and every GitHub user land here, silently.
|
|
58
60
|
*/
|
|
59
61
|
export const DEFAULT_TRACKER_PROVIDER = 'github';
|
|
60
|
-
/**
|
|
61
|
-
* The manifest key path, as ONE shared constant.
|
|
62
|
-
*
|
|
63
|
-
* Both readers must agree on this literal: the TypeScript reader
|
|
64
|
-
* (`readManifest().features.tracker.provider`) and the shell reader
|
|
65
|
-
* (`json_field_file "$devflowDir/manifest.json" "features.tracker.provider"`,
|
|
66
|
-
* whose jq and node backends both split the dotted path and walk it). A second
|
|
67
|
-
* spelling in a shell script is exactly the drift this constant prevents.
|
|
68
|
-
*/
|
|
69
|
-
export const TRACKER_PROVIDER_KEY_PATH = 'features.tracker.provider';
|
|
70
62
|
// ---------------------------------------------------------------------------
|
|
71
63
|
// Artifact basenames — one spelling for every TypeScript reader
|
|
72
64
|
//
|
|
73
65
|
// NOT the only spelling in the repository, and a rename that assumes it is will
|
|
74
|
-
// miss
|
|
66
|
+
// miss the places these names are hardcoded (PF-013): the SessionStart hook's
|
|
75
67
|
// Section 3 (shell) and the Tracker agent's prompt (prose), neither of which can
|
|
76
|
-
// import from here
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// uninstall-logic and core/tracker — so the spellings cannot drift silently, but
|
|
80
|
-
// they do have to move together.
|
|
68
|
+
// import from here. Each is cross-pinned against these constants by tests —
|
|
69
|
+
// shell-hooks-tracker, tracker-agent, uninstall-logic and core/tracker — so the
|
|
70
|
+
// spellings cannot drift silently, but they do have to move together.
|
|
81
71
|
//
|
|
82
|
-
// Two
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// Either spelled by hand leaves a file behind that no uninstall list accounts
|
|
89
|
-
// for, in a directory the run reports as swept.
|
|
72
|
+
// Two families are one-per-provider and are DERIVED from the registry rather
|
|
73
|
+
// than spelled out — the conventions files under TRACKER_CONVENTIONS_DIR and the
|
|
74
|
+
// attempt counters (TRACKER_ATTEMPTS_NAMES) — so a fourth provider is covered the
|
|
75
|
+
// day it joins the registry. The staging files are one-per-invocation under a
|
|
76
|
+
// mktemp name, so uninstall imports TRACKER_STAGED_PREFIX and resolves it against
|
|
77
|
+
// disk.
|
|
90
78
|
// ---------------------------------------------------------------------------
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
/**
|
|
80
|
+
* `~/.devflow/tracker/` — one inferred conventions file per provider
|
|
81
|
+
* (USER CONTENT on uninstall).
|
|
82
|
+
*
|
|
83
|
+
* D-TRACKER-PER-PROVIDER-CONVENTIONS: conventions are learned per PROVIDER, not
|
|
84
|
+
* per machine. A repository may select its own tracker in its committed
|
|
85
|
+
* `.devflow/project.json`, so one machine can meet more than one provider, and a
|
|
86
|
+
* single machine-wide file would be silently authoritative for whichever provider
|
|
87
|
+
* did not write it. One file per provider means a provider change moves nothing
|
|
88
|
+
* aside: the other provider's conventions stay where they are, correct for the
|
|
89
|
+
* repositories that use it.
|
|
90
|
+
*/
|
|
91
|
+
export const TRACKER_CONVENTIONS_DIR = 'tracker';
|
|
92
|
+
/**
|
|
93
|
+
* `~/.devflow/tracker.md` — the single machine-wide conventions file releases
|
|
94
|
+
* before per-provider conventions wrote. The `tracker-conventions-per-provider-v1`
|
|
95
|
+
* migration moves it to its provider's file; one it cannot place (no provider in
|
|
96
|
+
* its frontmatter, or that provider's file already exists) stays, as USER CONTENT.
|
|
97
|
+
*/
|
|
98
|
+
export const TRACKER_LEGACY_CONVENTIONS_FILE = 'tracker.md';
|
|
99
|
+
/** `~/.devflow/.tracker.attempts` — the single counter those releases kept; the migration removes it. */
|
|
100
|
+
export const TRACKER_LEGACY_ATTEMPTS_FILE = '.tracker.attempts';
|
|
101
|
+
/**
|
|
102
|
+
* `~/.devflow/.tracker.enabled` — the machine provider sentinel (install artifact).
|
|
103
|
+
* Holds the provider NAME on one line; absent for github (see applyTrackerSentinel).
|
|
104
|
+
*/
|
|
96
105
|
export const TRACKER_ENABLED_FILE = '.tracker.enabled';
|
|
97
106
|
/** `~/.devflow/.tracker.processing` — the Tracker agent's atomic claim (install artifact). */
|
|
98
107
|
export const TRACKER_CLAIM_FILE = '.tracker.processing';
|
|
@@ -114,6 +123,21 @@ export const TRACKER_CLAIM_FILE = '.tracker.processing';
|
|
|
114
123
|
* spellings together.
|
|
115
124
|
*/
|
|
116
125
|
export const TRACKER_STAGED_PREFIX = '.tracker-staged.';
|
|
126
|
+
/**
|
|
127
|
+
* `.tracker.{provider}.attempts` — one provider's inference attempt counter
|
|
128
|
+
* (install artifact).
|
|
129
|
+
*
|
|
130
|
+
* Per provider, so one provider whose tracker connection is broken spends only
|
|
131
|
+
* its own attempts: a machine that meets jira in one repository and linear in
|
|
132
|
+
* another must not have the broken one cap the working one. The claim file stays
|
|
133
|
+
* global — one Tracker agent runs at a time on a machine, whichever provider it
|
|
134
|
+
* is learning.
|
|
135
|
+
*/
|
|
136
|
+
export function trackerAttemptsName(provider) {
|
|
137
|
+
return `.tracker.${provider}.attempts`;
|
|
138
|
+
}
|
|
139
|
+
/** Every provider's attempt-counter basename, in registry order — what a re-arm clears and uninstall removes. */
|
|
140
|
+
export const TRACKER_ATTEMPTS_NAMES = TRACKER_PROVIDER_IDS.map(id => trackerAttemptsName(id));
|
|
117
141
|
/**
|
|
118
142
|
* How many background inference attempts a machine gets before the SessionStart
|
|
119
143
|
* hook stops emitting the setup directive.
|
|
@@ -165,7 +189,7 @@ export function isTrackerProvider(value) {
|
|
|
165
189
|
* Takes `unknown`, and a non-string renders as its TYPE: the module's
|
|
166
190
|
* never-throws contract (PF-014) has to hold for what reaches this sink, not
|
|
167
191
|
* only for what the signature says does — `devflow tracker --status` reads
|
|
168
|
-
*
|
|
192
|
+
* a conventions file's hand-editable frontmatter, and a caller-side guard is one edit
|
|
169
193
|
* from being gone. Naming the type also keeps the render total, where `String()`
|
|
170
194
|
* would hand control to a caller-supplied `toString` — itself both a throw path
|
|
171
195
|
* and an echo path this function exists to close.
|
|
@@ -240,68 +264,110 @@ export function normalizeTrackerFeature(raw) {
|
|
|
240
264
|
// ---------------------------------------------------------------------------
|
|
241
265
|
// Exported functions — path derivation (pure)
|
|
242
266
|
// ---------------------------------------------------------------------------
|
|
243
|
-
/** `{devflowDir}/tracker
|
|
244
|
-
export function
|
|
245
|
-
return path.join(devflowDir,
|
|
267
|
+
/** `{devflowDir}/tracker` — the directory holding one conventions file per provider. */
|
|
268
|
+
export function trackerConventionsDir(devflowDir) {
|
|
269
|
+
return path.join(devflowDir, TRACKER_CONVENTIONS_DIR);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `{devflowDir}/tracker/{provider}.md` — one provider's inferred conventions.
|
|
273
|
+
*
|
|
274
|
+
* The provider is a registry id (the type admits nothing else), so the segment it
|
|
275
|
+
* contributes is one of three fixed words, never user input joined into a path.
|
|
276
|
+
*/
|
|
277
|
+
export function trackerConventionsPath(devflowDir, provider) {
|
|
278
|
+
return path.join(trackerConventionsDir(devflowDir), `${provider}.md`);
|
|
246
279
|
}
|
|
247
|
-
/** `{devflowDir}/.tracker.attempts` —
|
|
248
|
-
export function trackerAttemptsPath(devflowDir) {
|
|
249
|
-
return path.join(devflowDir,
|
|
280
|
+
/** `{devflowDir}/.tracker.{provider}.attempts` — one provider's inference attempt counter. */
|
|
281
|
+
export function trackerAttemptsPath(devflowDir, provider) {
|
|
282
|
+
return path.join(devflowDir, trackerAttemptsName(provider));
|
|
250
283
|
}
|
|
251
|
-
/** `{devflowDir}/.tracker.enabled` — the
|
|
284
|
+
/** `{devflowDir}/.tracker.enabled` — the machine provider sentinel. */
|
|
252
285
|
export function trackerEnabledSentinelPath(devflowDir) {
|
|
253
286
|
return path.join(devflowDir, TRACKER_ENABLED_FILE);
|
|
254
287
|
}
|
|
288
|
+
/** How many leading lines of a conventions file are scanned for frontmatter. */
|
|
289
|
+
const FRONTMATTER_SCAN_LINES = 40;
|
|
255
290
|
/**
|
|
256
|
-
*
|
|
291
|
+
* Parse the leading frontmatter of a conventions file's head.
|
|
257
292
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* tests/core/tracker.test.ts pins it. Un-exporting would force the pin to
|
|
263
|
-
* re-derive the name from a template beside the real one, which is the
|
|
264
|
-
* shadow-reimplementation a guard is worth nothing without (PF-018).
|
|
293
|
+
* The one parser, shared by `devflow tracker --status` and the
|
|
294
|
+
* per-provider migration, so the two can never disagree about which provider a
|
|
295
|
+
* file names. Scans at most {@link FRONTMATTER_SCAN_LINES} lines; the first
|
|
296
|
+
* occurrence of each key wins.
|
|
265
297
|
*/
|
|
266
|
-
export function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
298
|
+
export function parseTrackerFrontmatter(head) {
|
|
299
|
+
const lines = head.split('\n', FRONTMATTER_SCAN_LINES);
|
|
300
|
+
if (lines[0]?.trim() !== '---')
|
|
301
|
+
return { hasFrontmatter: false };
|
|
302
|
+
let provider;
|
|
303
|
+
let inferredFrom;
|
|
304
|
+
for (const line of lines.slice(1)) {
|
|
305
|
+
if (line.trim() === '---')
|
|
306
|
+
break;
|
|
307
|
+
const match = /^([A-Za-z-]+):\s*(.*)$/.exec(line);
|
|
308
|
+
if (match === null)
|
|
309
|
+
continue;
|
|
310
|
+
if (match[1] === 'provider' && provider === undefined)
|
|
311
|
+
provider = match[2].trim();
|
|
312
|
+
if (match[1] === 'inferred-from' && inferredFrom === undefined)
|
|
313
|
+
inferredFrom = match[2].trim();
|
|
314
|
+
}
|
|
315
|
+
return { hasFrontmatter: true, provider, inferredFrom };
|
|
272
316
|
}
|
|
273
317
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
318
|
+
* How many leading BYTES of a conventions file any reader takes — the bound the
|
|
319
|
+
* Tracker agent writes to and the Git agent loads. A line cap alone bounds the
|
|
320
|
+
* SCAN, not the read: the file's size is not devflow's to assume (avoids PF-023:
|
|
321
|
+
* a bound is only real at the sink).
|
|
322
|
+
*/
|
|
323
|
+
export const TRACKER_CONVENTIONS_READ_BYTES = 8000;
|
|
324
|
+
/**
|
|
325
|
+
* Read at most `limit` bytes from the head of a REGULAR file.
|
|
280
326
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* too, and `renameStaleTrackerConventions` takes `previous` from the whole
|
|
286
|
-
* domain.
|
|
327
|
+
* `undefined` for an absent, unreadable or non-regular path — a FIFO or a device
|
|
328
|
+
* is refused before it is opened, so a hostile entry can never block a read.
|
|
329
|
+
* Follows a symlink to read what it names (a reader cares what the conventions
|
|
330
|
+
* SAY); it never moves or writes through one. Never throws (PF-014).
|
|
287
331
|
*/
|
|
288
|
-
export
|
|
332
|
+
export async function readBoundedHead(filePath, limit) {
|
|
333
|
+
let handle;
|
|
334
|
+
try {
|
|
335
|
+
if (!(await fs.stat(filePath)).isFile())
|
|
336
|
+
return undefined;
|
|
337
|
+
handle = await fs.open(filePath, 'r');
|
|
338
|
+
const buffer = Buffer.alloc(limit);
|
|
339
|
+
const { bytesRead } = await handle.read(buffer, 0, limit, 0);
|
|
340
|
+
return buffer.subarray(0, bytesRead).toString('utf-8');
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
await handle?.close().catch(() => undefined);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
289
349
|
// ---------------------------------------------------------------------------
|
|
290
350
|
// Exported functions — file lifecycle
|
|
291
351
|
// ---------------------------------------------------------------------------
|
|
292
352
|
/**
|
|
293
|
-
* Re-arm background convention inference by removing
|
|
353
|
+
* Re-arm background convention inference by removing every provider's attempt
|
|
354
|
+
* counter.
|
|
294
355
|
*
|
|
295
356
|
* The documented re-arm path (OD-14 / D-F): `devflow init` AND
|
|
296
|
-
* `devflow tracker --set/--status`
|
|
297
|
-
*
|
|
357
|
+
* `devflow tracker --set/--status` all re-arm, so a user whose tracker server
|
|
358
|
+
* was broken for five sessions is not stuck at the cap forever. Every provider's
|
|
359
|
+
* counter, not only the machine's: a repository's committed project.json can
|
|
360
|
+
* select a provider the machine never did, and its counter is the one a user in
|
|
361
|
+
* that repository is capped on.
|
|
298
362
|
*
|
|
299
|
-
* Idempotent when
|
|
363
|
+
* Idempotent when a counter is absent; never throws (PF-014). `fs.rm` with
|
|
300
364
|
* `force` treats an absent file — and an absent parent directory — as success.
|
|
301
365
|
*/
|
|
302
366
|
export async function rearmTrackerInference(devflowDir) {
|
|
303
367
|
try {
|
|
304
|
-
|
|
368
|
+
for (const name of TRACKER_ATTEMPTS_NAMES) {
|
|
369
|
+
await fs.rm(path.join(devflowDir, name), { force: true });
|
|
370
|
+
}
|
|
305
371
|
return { ok: true, value: undefined };
|
|
306
372
|
}
|
|
307
373
|
catch (err) {
|
|
@@ -309,13 +375,15 @@ export async function rearmTrackerInference(devflowDir) {
|
|
|
309
375
|
}
|
|
310
376
|
}
|
|
311
377
|
/**
|
|
312
|
-
* Converge the `.tracker.enabled`
|
|
378
|
+
* Converge the `.tracker.enabled` sentinel to the machine provider.
|
|
313
379
|
*
|
|
314
|
-
* [DR-10]
|
|
315
|
-
* removed when it is.
|
|
316
|
-
*
|
|
317
|
-
* `
|
|
318
|
-
*
|
|
380
|
+
* [DR-10] Holds the provider NAME, one line, whenever the machine provider is NOT
|
|
381
|
+
* github, and is removed when it is. The SessionStart hook reads it with the
|
|
382
|
+
* `read` builtin, so the machine provider costs no fork: a GitHub user pays one
|
|
383
|
+
* `stat`, and a jira machine whose conventions are already learned pays a stat,
|
|
384
|
+
* a builtin read and a second stat — zero forks either way. A name rather than a
|
|
385
|
+
* bare presence marker, so the hook never has to open the manifest to learn which
|
|
386
|
+
* provider it is gating.
|
|
319
387
|
*
|
|
320
388
|
* Converges unconditionally in both directions (avoids PF-015): a provider
|
|
321
389
|
* flipped back to github removes the sentinel in the same call shape that wrote
|
|
@@ -329,7 +397,7 @@ export async function applyTrackerSentinel(devflowDir, provider) {
|
|
|
329
397
|
}
|
|
330
398
|
else {
|
|
331
399
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
332
|
-
await fs.writeFile(sentinel,
|
|
400
|
+
await fs.writeFile(sentinel, `${provider}\n`, 'utf-8');
|
|
333
401
|
}
|
|
334
402
|
return { ok: true, value: undefined };
|
|
335
403
|
}
|
|
@@ -338,70 +406,89 @@ export async function applyTrackerSentinel(devflowDir, provider) {
|
|
|
338
406
|
}
|
|
339
407
|
}
|
|
340
408
|
/**
|
|
341
|
-
* Move
|
|
409
|
+
* Move the pre-per-provider `~/.devflow/tracker.md` to the provider file its
|
|
410
|
+
* frontmatter names (D-TRACKER-PER-PROVIDER-CONVENTIONS), once.
|
|
342
411
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
412
|
+
* - no legacy file → `none`
|
|
413
|
+
* - frontmatter names a registry provider,
|
|
414
|
+
* and that provider's file does not exist → `moved`, by `rename(2)`
|
|
415
|
+
* - no provider it can name, or the target
|
|
416
|
+
* already exists → `kept`: the file stays where it is,
|
|
417
|
+
* user content, with one reason
|
|
418
|
+
* - any I/O failure → `failed`
|
|
348
419
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
420
|
+
* `rename(2)` moves a SYMLINK itself, never the file it points at, so a
|
|
421
|
+
* conventions file kept in a dotfiles repository stays there and only the link
|
|
422
|
+
* moves. A link's target is resolved relative to the link's directory, so a
|
|
423
|
+
* RELATIVE link would resolve from one level deeper and name a different path —
|
|
424
|
+
* the conventions would silently vanish behind a dangling link that also blocks
|
|
425
|
+
* the Tracker agent's create-exclusive write. A relative link is therefore `kept`,
|
|
426
|
+
* with its reason; an absolute one moves unaffected. rename also replaces an
|
|
427
|
+
* existing destination without a word, which is why the target is probed first
|
|
428
|
+
* and a present one is never overwritten: that file holds conventions a user may
|
|
429
|
+
* have corrected by hand, and it is the provider's own.
|
|
351
430
|
*
|
|
352
|
-
* D-TRACKER-
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* for one provider keeps BOTH copies and says which one blocked the move; the
|
|
359
|
-
* user resolves it by moving one aside, and the next run completes the change.
|
|
360
|
-
* Numbering the backups was the alternative and was rejected: it accumulates
|
|
361
|
-
* without bound and puts names in `~/.devflow` that
|
|
362
|
-
* `TRACKER_CONVENTIONS_BACKUP_NAMES` cannot enumerate, leaving files no uninstall
|
|
363
|
-
* list accounts for. Hard links in this directory are already load-bearing — the
|
|
364
|
-
* Tracker agent places `tracker.md` itself with `ln` for the same
|
|
365
|
-
* create-exclusive property.
|
|
431
|
+
* D-TRACKER-MIGRATE-WINDOW: the probe and the rename are two calls, so a Tracker
|
|
432
|
+
* agent that writes the same provider's file between them is replaced by the
|
|
433
|
+
* legacy one. Accepted: the window is one syscall wide, only `devflow init` opens
|
|
434
|
+
* it, and both files hold that provider's conventions. link-then-unlink would
|
|
435
|
+
* close it but follows a symlink on macOS, fails where hard links are
|
|
436
|
+
* unsupported, and can leave both names behind.
|
|
366
437
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
438
|
+
* The legacy single attempt counter is removed on every run that reaches the end:
|
|
439
|
+
* it carries no user content, the per-provider counters replace it, and a file
|
|
440
|
+
* nothing reads is one no uninstall list would account for.
|
|
369
441
|
*/
|
|
370
|
-
export async function
|
|
371
|
-
|
|
372
|
-
return { kind: 'none' };
|
|
373
|
-
const from = trackerConventionsPath(devflowDir);
|
|
374
|
-
const to = trackerConventionsBackupPath(devflowDir, previous);
|
|
442
|
+
export async function migrateLegacyTrackerConventions(devflowDir) {
|
|
443
|
+
const from = path.join(devflowDir, TRACKER_LEGACY_CONVENTIONS_FILE);
|
|
375
444
|
try {
|
|
376
|
-
await fs.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
445
|
+
await fs.rm(path.join(devflowDir, TRACKER_LEGACY_ATTEMPTS_FILE), { force: true });
|
|
446
|
+
let isLink;
|
|
447
|
+
try {
|
|
448
|
+
isLink = (await fs.lstat(from)).isSymbolicLink();
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
if (errnoCode(err) === 'ENOENT')
|
|
383
452
|
return { kind: 'none' };
|
|
384
|
-
|
|
385
|
-
return {
|
|
386
|
-
kind: 'failed',
|
|
387
|
-
error: `Kept the existing ${to} — moving ${from} aside would have destroyed it. ` +
|
|
388
|
-
`Move or delete one of the two, then re-run to finish the provider change.`,
|
|
389
|
-
};
|
|
390
|
-
default:
|
|
391
|
-
return { kind: 'failed', error: `Could not move the stale tracker.md aside: ${errorMessage(err)}` };
|
|
453
|
+
throw err;
|
|
392
454
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
455
|
+
if (isLink && !path.isAbsolute(await fs.readlink(from))) {
|
|
456
|
+
return {
|
|
457
|
+
kind: 'kept',
|
|
458
|
+
reason: `${from} is a symbolic link with a relative target, which would no longer resolve from ` +
|
|
459
|
+
`${trackerConventionsDir(devflowDir)}, so it was left in place — re-create it there by hand.`,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const head = await readBoundedHead(from, TRACKER_CONVENTIONS_READ_BYTES);
|
|
463
|
+
if (head === undefined) {
|
|
464
|
+
return { kind: 'kept', reason: `${from} is not a readable regular file, so it was left in place.` };
|
|
465
|
+
}
|
|
466
|
+
const named = parseTrackerFrontmatter(head).provider;
|
|
467
|
+
if (!isTrackerProvider(named)) {
|
|
468
|
+
return {
|
|
469
|
+
kind: 'kept',
|
|
470
|
+
reason: `${from} names no tracker provider in its frontmatter, so it was left in place — ` +
|
|
471
|
+
`move it to ${trackerConventionsDir(devflowDir)}/{provider}.md by hand if it is still wanted.`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const to = trackerConventionsPath(devflowDir, named);
|
|
475
|
+
try {
|
|
476
|
+
await fs.lstat(to);
|
|
477
|
+
return {
|
|
478
|
+
kind: 'kept',
|
|
479
|
+
reason: `${from} was left in place — ${to} already exists and is never overwritten.`,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
if (errnoCode(err) !== 'ENOENT')
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
await fs.mkdir(trackerConventionsDir(devflowDir), { recursive: true });
|
|
487
|
+
await fs.rename(from, to);
|
|
488
|
+
return { kind: 'moved', from, to, provider: named };
|
|
396
489
|
}
|
|
397
490
|
catch (err) {
|
|
398
|
-
|
|
399
|
-
// there, so the reader's mismatch guard still fires and nothing was lost.
|
|
400
|
-
return {
|
|
401
|
-
kind: 'failed',
|
|
402
|
-
error: `Copied the stale conventions to ${to} but could not remove ${from}: ${errorMessage(err)}`,
|
|
403
|
-
};
|
|
491
|
+
return { kind: 'failed', error: `Could not move ${from} to its provider's file: ${errorMessage(err)}` };
|
|
404
492
|
}
|
|
405
|
-
return { kind: 'renamed', from, to, previous };
|
|
406
493
|
}
|
|
407
494
|
//# sourceMappingURL=tracker.js.map
|
|
@@ -2,14 +2,26 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dim } from '../colors.js';
|
|
5
|
+
/**
|
|
6
|
+
* The Claude Code directory: an absolute `CLAUDE_CONFIG_DIR`, else `~/.claude` —
|
|
7
|
+
* the rule `getClaudeDirectory()` applies (D-CLAUDE-CONFIG-DIR in
|
|
8
|
+
* src/targets/claude-code/claude-paths.ts), restated here so the HUD's copied
|
|
9
|
+
* import closure stays within src/hud and src/core. A relative value is ignored,
|
|
10
|
+
* not resolved against the session cwd, so the HUD counts what devflow installed.
|
|
11
|
+
*/
|
|
12
|
+
function userClaudeDir() {
|
|
13
|
+
const configured = process.env.CLAUDE_CONFIG_DIR;
|
|
14
|
+
if (configured !== undefined && configured !== '' && path.isAbsolute(configured))
|
|
15
|
+
return configured;
|
|
16
|
+
return path.join(process.env.HOME || homedir(), '.claude');
|
|
17
|
+
}
|
|
5
18
|
function countClaudeMdFiles(cwd) {
|
|
6
19
|
let count = 0;
|
|
7
20
|
// Check project CLAUDE.md
|
|
8
21
|
if (fs.existsSync(path.join(cwd, 'CLAUDE.md')))
|
|
9
22
|
count++;
|
|
10
23
|
// Check user CLAUDE.md
|
|
11
|
-
const claudeDir =
|
|
12
|
-
path.join(process.env.HOME || homedir(), '.claude');
|
|
24
|
+
const claudeDir = userClaudeDir();
|
|
13
25
|
if (fs.existsSync(path.join(claudeDir, 'CLAUDE.md')))
|
|
14
26
|
count++;
|
|
15
27
|
return count;
|
|
@@ -39,8 +51,7 @@ function countFromSettings(settingsPath) {
|
|
|
39
51
|
* Exported for use by the main HUD entry point.
|
|
40
52
|
*/
|
|
41
53
|
export function gatherConfigCounts(cwd) {
|
|
42
|
-
const claudeDir =
|
|
43
|
-
path.join(process.env.HOME || homedir(), '.claude');
|
|
54
|
+
const claudeDir = userClaudeDir();
|
|
44
55
|
const claudeMdFiles = countClaudeMdFiles(cwd);
|
|
45
56
|
// Count rules (.md/.mdc files in .claude/rules)
|
|
46
57
|
let rules = 0;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { dim } from '../colors.js';
|
|
3
3
|
import { getDecisionsLedgerPath } from '../../core/project-paths.js';
|
|
4
|
+
import { getLedgerRoot } from '../../core/ledger-root.js';
|
|
5
|
+
/** The HUD's per-git-command budget (src/hud/git.ts GIT_TIMEOUT). */
|
|
6
|
+
const LEDGER_ROOT_TIMEOUT_MS = 1000;
|
|
4
7
|
/**
|
|
5
8
|
* @devflow-design-decision D309
|
|
6
9
|
* Counts come from decisions-ledger.jsonl (the render source of truth), NOT
|
|
@@ -65,6 +68,17 @@ export function gatherLearningCounts(cwd) {
|
|
|
65
68
|
}
|
|
66
69
|
return parsedAny ? counts : null;
|
|
67
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Count the ledger the hooks write for a session started in `cwd`: the ledger
|
|
73
|
+
* root (getLedgerRoot — the main checkout in a linked worktree, the repository
|
|
74
|
+
* root from a subdirectory), or `cwd` itself outside a git work tree. One git
|
|
75
|
+
* call, bounded by the HUD's per-command budget; run it alongside the git
|
|
76
|
+
* status gather, not after it (D-LEDGER-MAIN-WORKTREE).
|
|
77
|
+
*/
|
|
78
|
+
export async function gatherLedgerLearningCounts(cwd, options = {}) {
|
|
79
|
+
const root = await getLedgerRoot(cwd, { ...options, timeoutMs: LEDGER_ROOT_TIMEOUT_MS });
|
|
80
|
+
return gatherLearningCounts(root ?? cwd);
|
|
81
|
+
}
|
|
68
82
|
/**
|
|
69
83
|
* HUD component: decisions/pitfalls counts.
|
|
70
84
|
* Shows how many active ADR/PF entries the project has accumulated.
|
package/dist/hud/config.js
CHANGED
|
@@ -23,7 +23,8 @@ export const HUD_COMPONENTS = [
|
|
|
23
23
|
'learningCounts',
|
|
24
24
|
];
|
|
25
25
|
export function getConfigPath() {
|
|
26
|
-
|
|
26
|
+
// D-ONE-HOME: always $HOME/.devflow — no environment variable relocates it.
|
|
27
|
+
const devflowDir = path.join(process.env.HOME || homedir(), '.devflow');
|
|
27
28
|
return path.join(devflowDir, 'hud.json');
|
|
28
29
|
}
|
|
29
30
|
export function loadConfig() {
|
package/dist/hud/cost-history.js
CHANGED
|
@@ -24,12 +24,10 @@ function isSessionEntry(value) {
|
|
|
24
24
|
let sessionsDirCreated = false;
|
|
25
25
|
let cachedAggregation = null;
|
|
26
26
|
/**
|
|
27
|
-
* Returns the paths used for cost storage.
|
|
28
|
-
* Respects DEVFLOW_DIR env for testability.
|
|
27
|
+
* Returns the paths used for cost storage, under $HOME/.devflow (D-ONE-HOME).
|
|
29
28
|
*/
|
|
30
29
|
export function getCostFilePaths() {
|
|
31
|
-
const devflowDir = process.env.
|
|
32
|
-
path.join(process.env.HOME || homedir(), '.devflow');
|
|
30
|
+
const devflowDir = path.join(process.env.HOME || homedir(), '.devflow');
|
|
33
31
|
const sessionsDir = path.join(devflowDir, 'costs', 'sessions');
|
|
34
32
|
const archivePath = path.join(devflowDir, 'costs', 'archive.jsonl');
|
|
35
33
|
return { sessionsDir, archivePath };
|