devflow-kit 2.5.0 → 3.0.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 +73 -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 +232 -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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* @file hook-log-dirs.ts
|
|
5
|
+
*
|
|
6
|
+
* The cap on the hooks' per-directory log folders.
|
|
7
|
+
*
|
|
8
|
+
* D-LOG-DIR-CAP: every hook logs under `~/.devflow/logs/<slug>/`, one directory
|
|
9
|
+
* per working directory a session ran in (`log-paths`' devflow_log_dir). Left
|
|
10
|
+
* uncapped they pile up — a machine was found holding 31k of them, most left by
|
|
11
|
+
* test runs in throwaway temp directories — so `devflow init` keeps the
|
|
12
|
+
* {@link MAX_HOOK_LOG_DIRS} most recently written and removes the rest, oldest
|
|
13
|
+
* first. A folder's recency is the newest mtime among the folder and the log
|
|
14
|
+
* files in it: a log that is only ever appended to leaves the folder's own mtime
|
|
15
|
+
* where the file's creation put it, so the folder mtime alone would rank the
|
|
16
|
+
* busiest project as the oldest.
|
|
17
|
+
*
|
|
18
|
+
* Every pass is bounded — at most {@link MAX_LOG_DIRS_SCANNED} folders read and
|
|
19
|
+
* {@link MAX_LOG_DIRS_PRUNED_PER_RUN} removed. The two bounds are equal, so one
|
|
20
|
+
* init clears every folder it scanned beyond the cap: a 31k backlog costs that
|
|
21
|
+
* init about seven seconds (roughly 2.2 s per 10,000 removals), and only a
|
|
22
|
+
* backlog larger than the scan bound is finished by the next init. The pass is
|
|
23
|
+
* the one-time cleanup of an existing backlog and the standing cap alike.
|
|
24
|
+
* Only directories are touched: files at the logs root (`proxy.log`) and symbolic
|
|
25
|
+
* links are left alone.
|
|
26
|
+
*/
|
|
27
|
+
/** How many hook log folders survive a prune — the most recently written ones. */
|
|
28
|
+
export const MAX_HOOK_LOG_DIRS = 200;
|
|
29
|
+
/** The most folders one prune reads; beyond it the rest wait for a later run. */
|
|
30
|
+
export const MAX_LOG_DIRS_SCANNED = 100_000;
|
|
31
|
+
/** The most folders one prune removes — every scanned folder, so one run clears what it read. */
|
|
32
|
+
export const MAX_LOG_DIRS_PRUNED_PER_RUN = MAX_LOG_DIRS_SCANNED;
|
|
33
|
+
/** The most entries read inside one folder to find its newest log. */
|
|
34
|
+
const MAX_FILES_READ_PER_DIR = 64;
|
|
35
|
+
/** Folders stat'ed or removed at once. */
|
|
36
|
+
const IO_CHUNK = 64;
|
|
37
|
+
/** The newest mtime among `dir` and the entries in it; -Infinity when unreadable. */
|
|
38
|
+
async function recencyOf(dir) {
|
|
39
|
+
let newest;
|
|
40
|
+
try {
|
|
41
|
+
newest = (await fs.lstat(dir)).mtimeMs;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return -Infinity;
|
|
45
|
+
}
|
|
46
|
+
let names;
|
|
47
|
+
try {
|
|
48
|
+
names = (await fs.readdir(dir)).slice(0, MAX_FILES_READ_PER_DIR);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return newest;
|
|
52
|
+
}
|
|
53
|
+
const times = await Promise.all(names.map(name => fs.lstat(path.join(dir, name)).then(st => st.mtimeMs, () => -Infinity)));
|
|
54
|
+
return Math.max(newest, ...times);
|
|
55
|
+
}
|
|
56
|
+
/** Apply `fn` to every item, `IO_CHUNK` at a time. Bounded by `items.length`. */
|
|
57
|
+
async function inChunks(items, fn) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for (let i = 0; i < items.length; i += IO_CHUNK) {
|
|
60
|
+
out.push(...await Promise.all(items.slice(i, i + IO_CHUNK).map(fn)));
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Remove the oldest hook log folders under `logsDir` beyond the cap
|
|
66
|
+
* (D-LOG-DIR-CAP). Never throws: a missing logs directory is nothing to do, and
|
|
67
|
+
* a folder that cannot be removed is counted as still over the cap.
|
|
68
|
+
*/
|
|
69
|
+
export async function pruneHookLogDirs(logsDir, opts = {}) {
|
|
70
|
+
const keep = opts.keep ?? MAX_HOOK_LOG_DIRS;
|
|
71
|
+
const maxRemovals = opts.maxRemovals ?? MAX_LOG_DIRS_PRUNED_PER_RUN;
|
|
72
|
+
const maxScanned = opts.maxScanned ?? MAX_LOG_DIRS_SCANNED;
|
|
73
|
+
if (!path.isAbsolute(logsDir) || path.basename(logsDir) !== 'logs') {
|
|
74
|
+
return { ok: false, error: `not a devflow logs directory: ${logsDir}` };
|
|
75
|
+
}
|
|
76
|
+
if (keep < 0 || maxRemovals < 0 || maxScanned < 0) {
|
|
77
|
+
return { ok: false, error: 'prune bounds must be non-negative' };
|
|
78
|
+
}
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(logsDir, { withFileTypes: true });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err.code === 'ENOENT')
|
|
85
|
+
return { ok: true, value: { removed: 0, overCap: 0 } };
|
|
86
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
87
|
+
}
|
|
88
|
+
const dirs = entries
|
|
89
|
+
.filter(e => e.isDirectory())
|
|
90
|
+
.slice(0, maxScanned)
|
|
91
|
+
.map(e => path.join(logsDir, e.name));
|
|
92
|
+
if (dirs.length <= keep)
|
|
93
|
+
return { ok: true, value: { removed: 0, overCap: 0 } };
|
|
94
|
+
const recencies = await inChunks(dirs, recencyOf);
|
|
95
|
+
const oldestFirst = dirs
|
|
96
|
+
.map((dir, i) => ({ dir, recency: recencies[i] }))
|
|
97
|
+
.sort((a, b) => a.recency - b.recency);
|
|
98
|
+
const overCap = oldestFirst.slice(0, oldestFirst.length - keep);
|
|
99
|
+
const batch = overCap.slice(0, maxRemovals);
|
|
100
|
+
const outcomes = await inChunks(batch, ({ dir }) => fs.rm(dir, { recursive: true, force: true }).then(() => true, () => false));
|
|
101
|
+
const removed = outcomes.filter(Boolean).length;
|
|
102
|
+
return { ok: true, value: { removed, overCap: overCap.length - removed } };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=hook-log-dirs.js.map
|
|
@@ -57,13 +57,15 @@ function readConfigFile(filePath) {
|
|
|
57
57
|
* Priority (highest wins): project config → global config → defaults.
|
|
58
58
|
*
|
|
59
59
|
* - Global: `~/.devflow/learning.json`
|
|
60
|
-
* - Project: `<
|
|
60
|
+
* - Project: `<ledgerRoot>/.devflow/learning/learning.json` — pass the ledger root
|
|
61
|
+
* (getLedgerRoot), where session-start-context reads it and `devflow learning
|
|
62
|
+
* --configure` writes it (D-LEDGER-MAIN-WORKTREE).
|
|
61
63
|
*
|
|
62
64
|
* Invalid JSON in either file is silently ignored and treated as absent.
|
|
63
65
|
*/
|
|
64
|
-
export function loadLearningTuningConfig(
|
|
66
|
+
export function loadLearningTuningConfig(ledgerRoot) {
|
|
65
67
|
const globalConfigPath = path.join(getDevFlowDirectory(), 'learning.json');
|
|
66
|
-
const projectConfigPath = getLearningTuningConfigPath(
|
|
68
|
+
const projectConfigPath = getLearningTuningConfigPath(ledgerRoot);
|
|
67
69
|
let config = { ...DEFAULTS };
|
|
68
70
|
const globalJson = readConfigFile(globalConfigPath);
|
|
69
71
|
if (globalJson !== null) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ledger-root.ts
|
|
3
|
+
*
|
|
4
|
+
* Where the learning ledger lives for a working directory — the TypeScript twin of
|
|
5
|
+
* `df_resolve_roots`' DF_LEDGER_ROOT in src/assets/scripts/hooks/resolve-project-root.
|
|
6
|
+
*
|
|
7
|
+
* D-LEDGER-MAIN-WORKTREE: the ledger is one per REPOSITORY, not per checkout. In a
|
|
8
|
+
* linked worktree the hooks queue and render into the main worktree's
|
|
9
|
+
* `.devflow/learning/`, so `devflow learning` and the HUD must read and clear that
|
|
10
|
+
* same directory — resolving the worktree's own toplevel (getGitRoot) would show an
|
|
11
|
+
* empty ledger and drain a queue nothing writes to. The rule, from ONE git call:
|
|
12
|
+
*
|
|
13
|
+
* git rev-parse --path-format=absolute --show-toplevel --git-common-dir
|
|
14
|
+
*
|
|
15
|
+
* - exactly two absolute lines, the common dir ends in `/.git`, `<main>/.devflow`
|
|
16
|
+
* is a directory and `<main>` is not HOME (physical paths, like the hooks'
|
|
17
|
+
* df_is_project_root) → `<main>`;
|
|
18
|
+
* - two absolute lines otherwise → the toplevel;
|
|
19
|
+
* - git before 2.31 echoes `--path-format=absolute` back as its own line → the
|
|
20
|
+
* toplevel it printed after it (the hooks fall back to the toplevel too);
|
|
21
|
+
* - git failed (not a work tree) → null: the caller keeps its non-git fallback;
|
|
22
|
+
* - any other output → getGitRoot(), the hooks' df_resolve_root fallback.
|
|
23
|
+
*
|
|
24
|
+
* Memory is NOT resolved here: working memory stays per checkout (getGitRoot).
|
|
25
|
+
* Parity with the shell helper is pinned by tests/core/ledger-root.test.ts, which
|
|
26
|
+
* runs df_resolve_roots on the same fixtures.
|
|
27
|
+
*/
|
|
28
|
+
import { execFile } from 'child_process';
|
|
29
|
+
import { promises as fs } from 'fs';
|
|
30
|
+
import * as os from 'os';
|
|
31
|
+
import * as path from 'path';
|
|
32
|
+
import { getGitRoot } from './git.js';
|
|
33
|
+
/** The one git call, as argv (no shell). */
|
|
34
|
+
const LEDGER_ROOT_GIT_ARGS = ['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir'];
|
|
35
|
+
/** What git before 2.31 prints first: the flag it does not know, echoed back. */
|
|
36
|
+
const PATH_FORMAT_ECHO = '--path-format=absolute';
|
|
37
|
+
/** Classify the git call's stdout. Pure. */
|
|
38
|
+
export function parseLedgerRootOutput(stdout) {
|
|
39
|
+
const lines = stdout.replace(/\n$/, '').split('\n');
|
|
40
|
+
if (lines.length === 2 && lines.every(l => path.isAbsolute(l))) {
|
|
41
|
+
return { kind: 'roots', toplevel: lines[0], commonDir: lines[1] };
|
|
42
|
+
}
|
|
43
|
+
if (lines.length === 3 && lines[0] === PATH_FORMAT_ECHO && path.isAbsolute(lines[1])) {
|
|
44
|
+
return { kind: 'toplevel', toplevel: lines[1] };
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'unrecognised' };
|
|
47
|
+
}
|
|
48
|
+
/** stdout of the one git call from `cwd`, or null when git failed. */
|
|
49
|
+
function runRevParse(cwd, timeoutMs) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
execFile('git', LEDGER_ROOT_GIT_ARGS, { cwd, timeout: timeoutMs, encoding: 'utf-8' }, (err, stdout) => {
|
|
52
|
+
resolve(err ? null : stdout);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** A path's physical location, or null when it cannot be resolved. */
|
|
57
|
+
async function physical(target) {
|
|
58
|
+
try {
|
|
59
|
+
return await fs.realpath(target);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether the main worktree `main` holds the ledger: it has a `.devflow/` directory
|
|
67
|
+
* and is not HOME. An unresolvable `main` is refused (the ledger stays in this
|
|
68
|
+
* checkout); an unresolvable HOME cannot equal it, so it refuses nothing — both as
|
|
69
|
+
* df_is_project_root decides.
|
|
70
|
+
*/
|
|
71
|
+
async function mainHoldsLedger(main, home) {
|
|
72
|
+
const stat = await fs.stat(path.join(main, '.devflow')).catch(() => null);
|
|
73
|
+
if (!stat?.isDirectory())
|
|
74
|
+
return false;
|
|
75
|
+
const [mainReal, homeReal] = await Promise.all([physical(main), physical(home)]);
|
|
76
|
+
return mainReal !== null && mainReal !== homeReal;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The learning ledger root for `cwd` — the main worktree or this checkout's
|
|
80
|
+
* toplevel — or null outside a git work tree.
|
|
81
|
+
*/
|
|
82
|
+
export async function getLedgerRoot(cwd = process.cwd(), options = {}) {
|
|
83
|
+
const stdout = await runRevParse(cwd, options.timeoutMs ?? 0);
|
|
84
|
+
if (stdout === null)
|
|
85
|
+
return null;
|
|
86
|
+
const parsed = parseLedgerRootOutput(stdout);
|
|
87
|
+
switch (parsed.kind) {
|
|
88
|
+
case 'toplevel':
|
|
89
|
+
return parsed.toplevel;
|
|
90
|
+
case 'unrecognised':
|
|
91
|
+
return getGitRoot(cwd);
|
|
92
|
+
case 'roots': {
|
|
93
|
+
if (!parsed.commonDir.endsWith('/.git'))
|
|
94
|
+
return parsed.toplevel;
|
|
95
|
+
const main = parsed.commonDir.slice(0, -'/.git'.length);
|
|
96
|
+
if (main && await mainHoldsLedger(main, options.home ?? os.homedir()))
|
|
97
|
+
return main;
|
|
98
|
+
return parsed.toplevel;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=ledger-root.js.map
|
package/dist/core/manifest.js
CHANGED
|
@@ -75,7 +75,6 @@ export async function readManifest(devflowDir) {
|
|
|
75
75
|
const features = data.features;
|
|
76
76
|
if (!data.version ||
|
|
77
77
|
!Array.isArray(data.plugins) ||
|
|
78
|
-
!data.scope ||
|
|
79
78
|
typeof features !== 'object' ||
|
|
80
79
|
features === null ||
|
|
81
80
|
typeof features.ambient !== 'boolean' ||
|
|
@@ -84,7 +83,7 @@ export async function readManifest(devflowDir) {
|
|
|
84
83
|
typeof data.updatedAt !== 'string') {
|
|
85
84
|
return null;
|
|
86
85
|
}
|
|
87
|
-
// D-FEATURES-ABSENT-ON (a sub-decision of D-FEATURES-
|
|
86
|
+
// D-FEATURES-ABSENT-ON (a sub-decision of D-FEATURES-NARROW-ONLY,
|
|
88
87
|
// src/core/feature-switch.ts): knowledge and learning are machine-wide
|
|
89
88
|
// switches, and every runtime gate — queue_read_gates in the hooks,
|
|
90
89
|
// isMachineFeatureOn in the CLI, the knowledge write-back prose gate — reads
|
|
@@ -135,7 +134,8 @@ export async function readManifest(devflowDir) {
|
|
|
135
134
|
const manifest = {
|
|
136
135
|
version: data.version,
|
|
137
136
|
plugins: data.plugins,
|
|
138
|
-
|
|
137
|
+
// D-MANIFEST-SCOPE-PINNED: the on-disk value is not consulted.
|
|
138
|
+
scope: 'user',
|
|
139
139
|
knownPlugins,
|
|
140
140
|
features: {
|
|
141
141
|
ambient: features.ambient,
|
|
@@ -194,7 +194,9 @@ export async function readManifest(devflowDir) {
|
|
|
194
194
|
export async function writeManifest(devflowDir, data) {
|
|
195
195
|
await fs.mkdir(devflowDir, { recursive: true });
|
|
196
196
|
const manifestPath = path.join(devflowDir, 'manifest.json');
|
|
197
|
-
|
|
197
|
+
// D-MANIFEST-SCOPE-PINNED: every write records 'user', whatever the caller holds.
|
|
198
|
+
const pinned = { ...data, scope: 'user' };
|
|
199
|
+
await writeFileAtomicExclusive(manifestPath, JSON.stringify(pinned, null, 2) + '\n');
|
|
198
200
|
}
|
|
199
201
|
/**
|
|
200
202
|
* Update a single feature field in the manifest. No-op when no manifest exists.
|
|
@@ -306,11 +306,10 @@ export const PR_HOST_OPS = [
|
|
|
306
306
|
* from the module that writes there: it is a fact about where PR mechanics live
|
|
307
307
|
* in the reference tree, not something the registry can work out.
|
|
308
308
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* linear user still opens pull requests.
|
|
309
|
+
* It is the provider-independent `pr/` directory, a sibling of the `tracker/`
|
|
310
|
+
* tree rather than a directory inside it: it is under no provider, and every
|
|
311
|
+
* install carries it because a jira or linear user still opens pull requests on
|
|
312
|
+
* GitHub.
|
|
314
313
|
*/
|
|
315
314
|
export const PR_HOST_DESTINATION_ROOT = 'pr';
|
|
316
315
|
/**
|
|
@@ -418,20 +417,6 @@ export const VARIANT_MODULES = [
|
|
|
418
417
|
export const MCP_BACKED_PROVIDER_SUBDIRS = ['tracker/jira', 'tracker/linear'];
|
|
419
418
|
/** The destination directory every tracker provider module lands under. */
|
|
420
419
|
export const TRACKER_DESTINATION_ROOT = 'tracker';
|
|
421
|
-
/**
|
|
422
|
-
* The tracker destination every install carries, whatever the user selected.
|
|
423
|
-
*
|
|
424
|
-
* Not a default and not a fallback: PR hosting stays on GitHub under every
|
|
425
|
-
* issue-tracker provider, so a jira or linear user still runs `gh pr` mechanics
|
|
426
|
-
* and still needs the GitHub tree reachable. It is the FLOOR of
|
|
427
|
-
* {@link installedReferenceManifest}'s union.
|
|
428
|
-
*
|
|
429
|
-
* Stated rather than derived, because the fact is about where pull requests
|
|
430
|
-
* live, not about anything the registry knows. A derivation from "the one
|
|
431
|
-
* CLI-backed module" would read as a rule and silently promote the next
|
|
432
|
-
* CLI-backed provider into everyone's install.
|
|
433
|
-
*/
|
|
434
|
-
export const PR_HOST_TRACKER_SUBDIR = `${TRACKER_DESTINATION_ROOT}/github`;
|
|
435
420
|
/**
|
|
436
421
|
* The provider-independent tool-call contract document.
|
|
437
422
|
*
|
|
@@ -668,93 +653,44 @@ export function expandVariants(modules = resolveVariantModules()) {
|
|
|
668
653
|
* build's own refusal sinks use (scripts/build-mds.ts).
|
|
669
654
|
*/
|
|
670
655
|
export function generatedReferenceManifest() {
|
|
671
|
-
|
|
672
|
-
if (!expanded.ok) {
|
|
673
|
-
throw new Error(`Reference module registry does not expand — ${JSON.stringify(expanded.error)}. ` +
|
|
674
|
-
`VARIANT_MODULES in src/core/mds-variants.ts is invalid.`);
|
|
675
|
-
}
|
|
676
|
-
return expanded.value.map(pair => pair.relPath);
|
|
656
|
+
return expandedManifest(resolveVariantModules());
|
|
677
657
|
}
|
|
678
658
|
/**
|
|
679
|
-
* The references ONE install carries
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* no provider directory, so it is named here rather than reached through the
|
|
695
|
-
* provider union;
|
|
696
|
-
* - a provider directory the user did not select is 11 files nothing they can
|
|
697
|
-
* reach ever loads (applies ADR-003 — ship the end state, not every state).
|
|
698
|
-
*
|
|
699
|
-
* `tracker/_mcp.md` rides the same gate its GENERATION does
|
|
700
|
-
* ({@link MCP_BACKED_PROVIDER_SUBDIRS}): it is the transport contract for
|
|
701
|
-
* providers reached by tool call, and GitHub's mechanics are `gh` commands. One
|
|
702
|
-
* predicate, asked of the selection here and of the registry in
|
|
703
|
-
* {@link mcpContractIsGenerated}, so opening the gate and shipping the provider
|
|
704
|
-
* stay the same edit.
|
|
705
|
-
*
|
|
706
|
-
* Derived from the registry rather than a provider table: a provider registered
|
|
707
|
-
* with a `tracker/{id}` subdir is installable by construction, and a literal
|
|
708
|
-
* here would be a second roster to keep in step with VARIANT_MODULES.
|
|
659
|
+
* The references ONE install carries — the manifest the overlay converges to.
|
|
660
|
+
*
|
|
661
|
+
* D-INSTALL-ALL-PROVIDERS: an install carries exactly what the build emits
|
|
662
|
+
* ({@link generatedReferenceManifest}): every provider's tree, the PR-host tree,
|
|
663
|
+
* the cross-cutting documents and `tracker/_mcp.md`, whatever the machine selected.
|
|
664
|
+
* The provider is no longer a property of the install. A repository selects its
|
|
665
|
+
* own tracker in its committed `.devflow/project.json`, so one machine meets more
|
|
666
|
+
* than one provider and a Git spawn must find the mechanics of whichever one the
|
|
667
|
+
* repository resolves — an install scoped to the machine's selection would leave
|
|
668
|
+
* every such repository degraded until someone re-ran init for a provider that is
|
|
669
|
+
* not theirs. The trees are INERT until a spawn resolves a provider and names
|
|
670
|
+
* one of its files, so carrying all of them costs disk, never context.
|
|
671
|
+
*
|
|
672
|
+
* The overlay still PRUNES everything under its converged subtrees this manifest
|
|
673
|
+
* does not name, so a retired generated document leaves on the next install.
|
|
709
674
|
*
|
|
710
675
|
* Asserts rather than degrades on a registry that does not expand, exactly as
|
|
711
676
|
* its sibling does (design review M3): the registry is a compile-time constant,
|
|
712
|
-
* so a refusal is a programming error rather than an install-time degradation
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
* `tracker/{id}` sub-directory key.
|
|
718
|
-
* @param opts.modules - Registry to expand (defaults to the shipped one).
|
|
719
|
-
* Injectable so both the refusal arm and a provider set this build does not
|
|
720
|
-
* produce are provable without editing the registry.
|
|
677
|
+
* so a refusal is a programming error rather than an install-time degradation.
|
|
678
|
+
*
|
|
679
|
+
* @param opts.modules - Registry to expand (defaults to the shipped one, with the
|
|
680
|
+
* gated contract module resolved). Injectable so the refusal arm is provable
|
|
681
|
+
* without editing the registry.
|
|
721
682
|
*/
|
|
722
|
-
export function installedReferenceManifest(opts) {
|
|
723
|
-
|
|
683
|
+
export function installedReferenceManifest(opts = {}) {
|
|
684
|
+
return expandedManifest(opts.modules ?? resolveVariantModules());
|
|
685
|
+
}
|
|
686
|
+
/** Every relPath a registry expands to, or the assertion both manifests share. */
|
|
687
|
+
function expandedManifest(modules) {
|
|
724
688
|
const expanded = expandVariants(modules);
|
|
725
689
|
if (!expanded.ok) {
|
|
726
690
|
throw new Error(`Reference module registry does not expand — ${JSON.stringify(expanded.error)}. ` +
|
|
727
691
|
`VARIANT_MODULES in src/core/mds-variants.ts is invalid.`);
|
|
728
692
|
}
|
|
729
|
-
|
|
730
|
-
const wanted = new Set(['', PR_HOST_DESTINATION_ROOT, PR_HOST_TRACKER_SUBDIR, providerSubdir]);
|
|
731
|
-
const installed = expanded.value
|
|
732
|
-
.filter(pair => wanted.has(subdirOfRelPath(pair.relPath)))
|
|
733
|
-
.map(pair => pair.relPath);
|
|
734
|
-
const gated = MCP_BACKED_PROVIDER_SUBDIRS;
|
|
735
|
-
if (gated.includes(providerSubdir)) {
|
|
736
|
-
const contract = contractRelPath(expanded.value);
|
|
737
|
-
if (contract !== undefined)
|
|
738
|
-
installed.push(contract);
|
|
739
|
-
}
|
|
740
|
-
return installed;
|
|
741
|
-
}
|
|
742
|
-
/** The directory part of a manifest-relative path; `''` for a file at the root. */
|
|
743
|
-
function subdirOfRelPath(relPath) {
|
|
744
|
-
const cut = relPath.lastIndexOf('/');
|
|
745
|
-
return cut < 0 ? '' : relPath.slice(0, cut);
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* The tool-call contract's emitted path, as this registry expands it — read from
|
|
749
|
-
* the expansion rather than composed from the module's fields, so the name can
|
|
750
|
-
* only ever be the one the build actually writes.
|
|
751
|
-
*
|
|
752
|
-
* Takes the already-expanded pairs rather than re-expanding: the caller has
|
|
753
|
-
* already validated the same registry expands cleanly, so a second call would
|
|
754
|
-
* only duplicate that work and reintroduce a refusal branch that can never fire.
|
|
755
|
-
*/
|
|
756
|
-
function contractRelPath(pairs) {
|
|
757
|
-
return pairs.find(pair => pair.module === MCP_CONTRACT_MODULE.source)?.relPath;
|
|
693
|
+
return expanded.value.map(pair => pair.relPath);
|
|
758
694
|
}
|
|
759
695
|
// ---------------------------------------------------------------------------
|
|
760
696
|
// Section splitting — which slice of a module's compiled body belongs to which op
|
|
@@ -792,8 +728,9 @@ export const VARIANT_SECTION_MARKER_RE = /^<!-- op: (_?[a-z0-9][a-z0-9._-]{0,63}
|
|
|
792
728
|
*
|
|
793
729
|
* empty-section is the third arm, and it exists because the other two cannot see
|
|
794
730
|
* it: an op with a marker and no body compiles cleanly and emits a zero-byte
|
|
795
|
-
* reference, which
|
|
796
|
-
* signal at all (the GAP-44 shape — omission is caught, emptiness
|
|
731
|
+
* reference, which the agent then loads as an operation with no instructions,
|
|
732
|
+
* with no build signal at all (the GAP-44 shape — omission is caught, emptiness
|
|
733
|
+
* is not).
|
|
797
734
|
*
|
|
798
735
|
* Total on success, and immutable: the caller gets back a readonly array of its
|
|
799
736
|
* OWN records, in its own order, each carrying its section. Nothing is looked up
|
package/dist/core/migrations.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { promises as fs } from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import * as os from 'os';
|
|
14
13
|
import { writeFileAtomicExclusive } from './fs-atomic.js';
|
|
15
14
|
import { getMemoryDir } from './project-paths.js';
|
|
16
15
|
import { LEGACY_AGENT_KEYS, canonicaliseAgentKeys, parseAgentMappingEnvelope } from './agent-models.js';
|
|
16
|
+
import { migrateLegacyTrackerConventions } from './tracker.js';
|
|
17
17
|
/**
|
|
18
18
|
* D31: Registry pattern over scattered `if (!applied.includes(...))` conditionals.
|
|
19
19
|
*
|
|
@@ -94,23 +94,48 @@ export const MIGRATIONS = [
|
|
|
94
94
|
return { infos, warnings };
|
|
95
95
|
},
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
id: 'tracker-conventions-per-provider-v1',
|
|
99
|
+
description: 'Move ~/.devflow/tracker.md to ~/.devflow/tracker/{provider}.md, the provider its frontmatter names',
|
|
100
|
+
scope: 'global',
|
|
101
|
+
// D-TRACKER-PER-PROVIDER-CONVENTIONS: conventions became per provider, so the
|
|
102
|
+
// single machine-wide file moves to the file of the provider it was learned
|
|
103
|
+
// for. The move and every refusal to move are migrateLegacyTrackerConventions'
|
|
104
|
+
// (src/core/tracker.ts); this entry maps its outcome onto the runner's
|
|
105
|
+
// contract. A file it leaves in place is REPORTED, once, and the migration is
|
|
106
|
+
// marked applied — the file is user content and nothing a re-run could do
|
|
107
|
+
// differently. An I/O failure THROWS instead: a throwing migration is not
|
|
108
|
+
// marked applied, so the runner retries it on the next `devflow init` rather
|
|
109
|
+
// than recording a move that never happened (the retry-forever path the
|
|
110
|
+
// KNOWN ISSUE above describes is bounded here by what can fail — a rename and
|
|
111
|
+
// an rm inside ~/.devflow).
|
|
112
|
+
async run(ctx) {
|
|
113
|
+
const outcome = await migrateLegacyTrackerConventions(ctx.devflowDir);
|
|
114
|
+
switch (outcome.kind) {
|
|
115
|
+
case 'none':
|
|
116
|
+
return { infos: [], warnings: [] };
|
|
117
|
+
case 'moved':
|
|
118
|
+
return { infos: [`Moved the ${outcome.provider} tracker conventions to ${outcome.to}`], warnings: [] };
|
|
119
|
+
case 'kept':
|
|
120
|
+
return { infos: [], warnings: [`tracker-conventions-per-provider-v1: ${outcome.reason}`] };
|
|
121
|
+
case 'failed':
|
|
122
|
+
throw new Error(outcome.error);
|
|
123
|
+
default: {
|
|
124
|
+
const _exhaustive = outcome;
|
|
125
|
+
return _exhaustive;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
},
|
|
97
130
|
];
|
|
98
131
|
const MIGRATIONS_FILE = 'migrations.json';
|
|
99
132
|
/**
|
|
100
|
-
* D30: State lives at `~/.devflow/migrations.json`
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* `~/.devflow/manifest.json` while local-scope manifests live at
|
|
105
|
-
* `.devflow/manifest.json` inside the repo. A migration that runs on user-scope
|
|
106
|
-
* init wouldn't be recorded in a local-scope manifest, so the migration would
|
|
107
|
-
* re-run on the next local-scope init.
|
|
108
|
-
* - Migration state is machine-wide: once a global migration runs on a machine it
|
|
109
|
-
* should never re-run regardless of which project or scope triggered devflow init.
|
|
110
|
-
* - `~/.devflow/migrations.json` is always writable (home-dir location), whereas
|
|
111
|
-
* local-scope devflowDir may be inside a read-only checkout.
|
|
133
|
+
* D30: State lives at `~/.devflow/migrations.json` rather than in the install
|
|
134
|
+
* manifest because migration state is machine-wide: once a global migration runs
|
|
135
|
+
* on a machine it should never re-run regardless of which project triggered
|
|
136
|
+
* devflow init.
|
|
112
137
|
*
|
|
113
|
-
* @param devflowDir - absolute path to
|
|
138
|
+
* @param devflowDir - absolute path to the resolved machine root (`~/.devflow`)
|
|
114
139
|
*/
|
|
115
140
|
export async function readAppliedMigrations(devflowDir) {
|
|
116
141
|
const filePath = path.join(devflowDir, MIGRATIONS_FILE);
|
|
@@ -227,7 +252,7 @@ async function runGlobalMigration(migration, ctx) {
|
|
|
227
252
|
* additional projects) can retry the failed projects.
|
|
228
253
|
*
|
|
229
254
|
* D37: runPerProjectMigration is unreachable in production — MIGRATIONS holds
|
|
230
|
-
* only
|
|
255
|
+
* only global migrations. The vacuous-truth
|
|
231
256
|
* analysis is preserved for correctness: if a per-project migration is ever
|
|
232
257
|
* added, an empty discoveredProjects list marks it applied (empty-discovery-marks-applied
|
|
233
258
|
* intended); the applied-set write is skipped only when newlyApplied is empty,
|
|
@@ -267,19 +292,20 @@ async function runPerProjectMigration(migration, ctx, discoveredProjects) {
|
|
|
267
292
|
* Run all unapplied migrations from MIGRATIONS.
|
|
268
293
|
*
|
|
269
294
|
* D32: Always-run-unapplied semantics (no fresh-vs-upgrade branch).
|
|
270
|
-
* MIGRATIONS currently holds
|
|
271
|
-
*
|
|
272
|
-
*
|
|
295
|
+
* MIGRATIONS currently holds only global migrations; on a fresh machine each
|
|
296
|
+
* executes once and the applied set is written to migrations.json. On subsequent
|
|
297
|
+
* runs every ID is already in the applied set and the loop is a no-op.
|
|
273
298
|
*
|
|
274
|
-
* @param ctx - devflowDir
|
|
299
|
+
* @param ctx - devflowDir, the resolved machine root (`~/.devflow`) that also holds
|
|
300
|
+
* migrations.json; memoryDir and projectRoot are filled per-project
|
|
275
301
|
* @param discoveredProjects - absolute paths to discovered Claude-enabled project roots
|
|
276
302
|
* @param registryOverride - override MIGRATIONS for testing (defaults to module-level MIGRATIONS)
|
|
277
303
|
*/
|
|
278
304
|
export async function runMigrations(ctx, discoveredProjects, registryOverride) {
|
|
279
305
|
const registry = registryOverride ?? MIGRATIONS;
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
const appliedArray = await readAppliedMigrations(
|
|
306
|
+
// D-ONE-HOME: state lives in the caller's resolved machine root, never a
|
|
307
|
+
// second, independently derived home path (D30: machine-wide state).
|
|
308
|
+
const appliedArray = await readAppliedMigrations(ctx.devflowDir);
|
|
283
309
|
// Convert to Set once for O(1) lookups throughout the loop (issue #9)
|
|
284
310
|
const applied = new Set(appliedArray);
|
|
285
311
|
const newlyApplied = [];
|
|
@@ -323,7 +349,7 @@ export async function runMigrations(ctx, discoveredProjects, registryOverride) {
|
|
|
323
349
|
}
|
|
324
350
|
// Write state once at end, accumulating all newly applied IDs (issue #5 — O(N²) → O(1))
|
|
325
351
|
if (newlyApplied.length > 0) {
|
|
326
|
-
await writeAppliedMigrations(
|
|
352
|
+
await writeAppliedMigrations(ctx.devflowDir, [...appliedArray, ...newlyApplied]);
|
|
327
353
|
}
|
|
328
354
|
return { newlyApplied, failures, infos, warnings };
|
|
329
355
|
}
|
package/dist/core/plugins.js
CHANGED
|
@@ -492,7 +492,8 @@ export const FEATURE_OWNED_RULES = ['compliance'];
|
|
|
492
492
|
* command-less plugin, so a reference to one is a reference to something the
|
|
493
493
|
* user may deliberately not have. The referencing prompts are written to probe
|
|
494
494
|
* first and proceed without it — `/code-review` checks
|
|
495
|
-
*
|
|
495
|
+
* `skills/devflow:{focus}/SKILL.md` under Claude Code's directory (`CLAUDE_CONFIG_DIR`
|
|
496
|
+
* when absolute, else `~/.claude` — D-CLAUDE-DIR-PROMPTS) before spawning that focus, the
|
|
496
497
|
* Review and Code agents continue when the Skill invocation fails. Putting them
|
|
497
498
|
* in a `requires` would reinstate the universal install for exactly the eight
|
|
498
499
|
* skills the selection prompt exists to let a user decline (AC-25).
|
|
@@ -832,9 +833,9 @@ export const WORKFLOW_ORDER = [
|
|
|
832
833
|
];
|
|
833
834
|
/**
|
|
834
835
|
* Plugin names excluded from the init multiselect buckets.
|
|
835
|
-
*
|
|
836
|
+
* init adds them itself rather than offering them:
|
|
836
837
|
* - devflow-core-skills (always installed, non-optional)
|
|
837
|
-
* - devflow-ambient (
|
|
838
|
+
* - devflow-ambient (installed iff ambient mode is on — D-AMBIENT-FOLLOWS-SWITCH)
|
|
838
839
|
*
|
|
839
840
|
* Invariant: EXCLUDED ∩ optional === ∅ — no optional plugin may be excluded from
|
|
840
841
|
* the init UI without a re-init carry mechanism to preserve it across full reinstalls.
|
|
@@ -847,7 +848,7 @@ export const EXCLUDED = new Set(['devflow-core-skills', 'devflow-ambient']);
|
|
|
847
848
|
*
|
|
848
849
|
* Excluded from both buckets (not selectable at init):
|
|
849
850
|
* - devflow-core-skills (always installed)
|
|
850
|
-
* - devflow-ambient (
|
|
851
|
+
* - devflow-ambient (installed iff ambient mode is on)
|
|
851
852
|
*
|
|
852
853
|
* Pure function — does not mutate the input array; preserves DEVFLOW_PLUGINS
|
|
853
854
|
* ordering within each bucket; deterministic; no I/O.
|
|
@@ -139,21 +139,4 @@ export function getResearchDir(projectRoot) {
|
|
|
139
139
|
export function getHandoffPath(projectRoot, branchSlug) {
|
|
140
140
|
return path.join(projectRoot, '.devflow', 'docs', `handoff-${branchSlug}.md`);
|
|
141
141
|
}
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Gitignore entries (returned as string arrays for updateGitignore callers)
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
/**
|
|
146
|
-
* The canonical list of generic gitignore entries Devflow adds to a project's
|
|
147
|
-
* root .gitignore for LOCAL-scope installs. Currently just `.claude/`.
|
|
148
|
-
*
|
|
149
|
-
* `.devflow/` is intentionally NOT here: it is managed by ensureDevflowGitignore
|
|
150
|
-
* (TS) / ensure-root-gitignore (hook), which write the feature-knowledge carve-out
|
|
151
|
-
* for ALL scopes. Adding a bare `.devflow/` here would append a wholesale-ignore
|
|
152
|
-
* line after the carve-out and re-bury it (last match wins in .gitignore).
|
|
153
|
-
*
|
|
154
|
-
* CJS mirror: src/assets/scripts/hooks/lib/project-paths.cjs getGitignoreEntries().
|
|
155
|
-
*/
|
|
156
|
-
export function getGitignoreEntries() {
|
|
157
|
-
return ['.claude/'];
|
|
158
|
-
}
|
|
159
142
|
//# sourceMappingURL=project-paths.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Whether two paths name one location — realpaths where they exist, so a symlinked
|
|
5
|
+
* HOME, or macOS's /var → /private/var temp tree, still matches. The shell hooks make
|
|
6
|
+
* the same physical comparison in git-marker's df_is_project_root (D-HOOKS-GIT-ONLY).
|
|
7
|
+
*/
|
|
8
|
+
export async function isSameLocation(a, b) {
|
|
9
|
+
const canonical = (target) => fs.realpath(target).catch(() => path.resolve(target));
|
|
10
|
+
const [left, right] = await Promise.all([canonical(a), canonical(b)]);
|
|
11
|
+
return left === right;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* D-INIT-NOT-HOME: the CLI half of D-HOOKS-GIT-ONLY. A git repository rooted at
|
|
15
|
+
* HOME (a dotfiles repo) is not a project: its `<root>/.devflow` is the machine
|
|
16
|
+
* root ~/.devflow, and its `.gitignore` and `.claudeignore` are the user's own
|
|
17
|
+
* home-directory files. So no command writes per-repository files there — the
|
|
18
|
+
* same rule the hooks' df_is_project_root applies — and `roots` is returned
|
|
19
|
+
* without any entry that is HOME.
|
|
20
|
+
*/
|
|
21
|
+
export async function withoutHomeRoots(roots, homeDir) {
|
|
22
|
+
const verdicts = await Promise.all(roots.map(root => isSameLocation(root, homeDir)));
|
|
23
|
+
return roots.filter((_, i) => !verdicts[i]);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=same-location.js.map
|