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.
Files changed (94) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +5 -5
  3. package/dist/agents/code.md +4 -4
  4. package/dist/agents/fix.md +1 -1
  5. package/dist/agents/git.md +2 -2
  6. package/dist/agents/skim.md +1 -1
  7. package/dist/agents/triage.md +7 -0
  8. package/dist/cli/commands/curation.js +67 -16
  9. package/dist/cli/commands/gate-level-prompts.js +8 -6
  10. package/dist/cli/commands/gates.js +5 -4
  11. package/dist/cli/commands/init.js +8 -7
  12. package/dist/cli/commands/memory.js +59 -12
  13. package/dist/cli/commands/uninstall.js +77 -9
  14. package/dist/commands/bug-analysis.md +2 -2
  15. package/dist/commands/code-review.md +11 -11
  16. package/dist/commands/debug.md +2 -2
  17. package/dist/commands/dynamic-build.md +2 -2
  18. package/dist/commands/dynamic-plan.md +2 -2
  19. package/dist/commands/dynamic-profile.md +2 -2
  20. package/dist/commands/dynamic-tickets.md +2 -2
  21. package/dist/commands/explore.md +2 -2
  22. package/dist/commands/implement.md +2 -2
  23. package/dist/commands/plan.md +2 -2
  24. package/dist/commands/release.md +2 -2
  25. package/dist/commands/research.md +2 -2
  26. package/dist/commands/resolve.md +16 -16
  27. package/dist/commands/self-review.md +2 -2
  28. package/dist/core/area-pause-notice.js +44 -0
  29. package/dist/core/curation-move-skip.js +4 -5
  30. package/dist/core/curation-queue-cleanup.js +6 -5
  31. package/dist/core/curation-store.js +1 -0
  32. package/dist/core/evidence-policy.js +10 -8
  33. package/dist/core/plugins.js +4 -4
  34. package/dist/core/tracked-areas.js +138 -14
  35. package/dist/curation-off/agents/code.md +4 -4
  36. package/dist/curation-off/agents/fix.md +1 -1
  37. package/dist/curation-off/agents/triage.md +7 -0
  38. package/dist/curation-off/commands/code-review.md +11 -11
  39. package/dist/curation-off/commands/debug.md +2 -2
  40. package/dist/curation-off/commands/dynamic-build.md +2 -2
  41. package/dist/curation-off/commands/explore.md +2 -2
  42. package/dist/curation-off/commands/implement.md +2 -2
  43. package/dist/curation-off/commands/plan.md +2 -2
  44. package/dist/curation-off/commands/resolve.md +16 -16
  45. package/dist/curation-off/commands/self-review.md +2 -2
  46. package/dist/hud/components/curation-counts.js +27 -0
  47. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  48. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  49. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  50. package/dist/skills/git/references/publication-gate.md +6 -7
  51. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  52. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  53. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  54. package/package.json +1 -1
  55. package/src/assets/agents/code.mds +4 -4
  56. package/src/assets/agents/evaluate.md +6 -12
  57. package/src/assets/agents/fix.mds +1 -1
  58. package/src/assets/agents/git.mds +2 -2
  59. package/src/assets/agents/skim.mds +1 -1
  60. package/src/assets/agents/triage.mds +7 -0
  61. package/src/assets/agents/validate.md +20 -27
  62. package/src/assets/commands/_partials/_decisions.mds +10 -8
  63. package/src/assets/commands/_partials/_settings.mds +2 -2
  64. package/src/assets/commands/code-review.mds +9 -9
  65. package/src/assets/commands/resolve.mds +14 -14
  66. package/src/assets/mds/git/_ops.mds +6 -1
  67. package/src/assets/mds/git/_ops_pr.mds +20 -10
  68. package/src/assets/mds/git/_ops_steps.mds +10 -0
  69. package/src/assets/mds/git/_pr.mds +14 -14
  70. package/src/assets/mds/git/_references.mds +6 -7
  71. package/src/assets/mds/tracker/_github.mds +1 -4
  72. package/src/assets/mds/tracker/_jira.mds +1 -4
  73. package/src/assets/mds/tracker/_linear.mds +1 -4
  74. package/src/assets/scripts/ci-wait.cjs +389 -92
  75. package/src/assets/scripts/hooks/background-memory-update +79 -1
  76. package/src/assets/scripts/hooks/json-helper.cjs +44 -2
  77. package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
  78. package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
  79. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
  80. package/src/assets/scripts/hooks/memory-permission +10 -2
  81. package/src/assets/scripts/hooks/memory-worker +11 -0
  82. package/src/assets/scripts/hooks/pre-compact-memory +11 -0
  83. package/src/assets/scripts/hooks/queue-append +218 -1
  84. package/src/assets/scripts/hooks/session-start-context +89 -37
  85. package/src/assets/scripts/hooks/session-start-memory +39 -0
  86. package/src/assets/scripts/pr-evidence.cjs +5 -2
  87. package/src/assets/scripts/resolve-settings.cjs +244 -30
  88. package/src/assets/scripts/verify-evidence.cjs +307 -75
  89. package/src/assets/skills/agent-references/SKILL.md +7 -1
  90. package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
  91. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
  92. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  93. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  94. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
@@ -22,7 +22,7 @@
22
22
  // `features.gateLevel` default
23
23
  //
24
24
  // D-SETTINGS-LOCAL-ONLY: local git subprocesses only, and never gh, never the
25
- // network, never a git command that refreshes the index. At most four, in order:
25
+ // network, never a git command that refreshes the index. At most these, in order:
26
26
  // git rev-parse --show-toplevel always
27
27
  // git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- <PERSONAL_TRACKING_PATHSPECS>
28
28
  // only when config.json exists;
@@ -32,6 +32,17 @@
32
32
  // git symbolic-ref --quiet refs/remotes/origin/HEAD in a repository
33
33
  // git cat-file blob refs/remotes/origin/<D>:.devflow/project.json
34
34
  // only when origin/HEAD names D
35
+ // git rev-parse --path-format=absolute --git-common-dir only when MEMORY or CURATION is
36
+ // still on and <toplevel>/.git is
37
+ // not a folder: it names the ledger root
38
+ // git ... ls-files -z -- <the pathspecs of an area> only for a switch still on: ONE call
39
+ // for both areas when their roots are
40
+ // equal, one more per area only when it
41
+ // lists something; otherwise one per root
42
+ // MEMORY=off while git tracks the memory folder at the toplevel (or does not answer for it),
43
+ // and CURATION=off while git tracks the curation folders at the ledger root (or does not
44
+ // answer for them). The answer only narrows a switch; the leaf lib hooks/lib/tracked-areas.cjs
45
+ // holds the pathspecs, the scrub of redirecting variables and the mapping of git's answer.
35
46
  // The team's evidence FLOOR is the default branch's and is
36
47
  // resolve-evidence-policy.cjs's job (it runs over the network, from commands).
37
48
  // This script reads the WORKTREE's copy for every other key, so a branch that
@@ -92,6 +103,7 @@ const fs = require('fs');
92
103
  const path = require('path');
93
104
  const childProcess = require('child_process');
94
105
  const projectConfig = require('./lib/project-config.cjs');
106
+ const trackedAreas = require('./hooks/lib/tracked-areas.cjs');
95
107
 
96
108
  const {
97
109
  PUBLICATIONS,
@@ -188,15 +200,14 @@ const SETTINGS_LINE_RE = new RegExp(
188
200
  * REVIEW_PUBLICATION=off nothing is published (raised to a stub under required)
189
201
  * COMPLIANCE=generic the review lens still runs
190
202
  * KNOWLEDGE=off no knowledge write-back commits into the repository
191
- * MEMORY/CURATION=on the machine switch is the one that turns them off;
192
- * hooks never consume this line (their gate folds an
193
- * unreadable file as narrowing nothing)
203
+ * MEMORY/CURATION=off a resolver that cannot say which repository it is in cannot
204
+ * tell whether git tracks their folders, so both stay off
194
205
  * GATE_LEVEL=full the most checking applies: a resolver that cannot read
195
206
  * its inputs never lowers the level
196
207
  */
197
208
  const SETTINGS_FAIL_CLOSED_LINE =
198
209
  'TRACKER=github TRACKER_SOURCE=default TRACKER_WARN=invalid SITE=none KEY=none '
199
- + 'REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=on CURATION=on KNOWLEDGE=off GATE_LEVEL=full';
210
+ + 'REVIEW_PUBLICATION=off COMPLIANCE=generic MEMORY=off CURATION=off KNOWLEDGE=off GATE_LEVEL=full';
200
211
 
201
212
  /** Bound on each local git call. */
202
213
  const GIT_TIMEOUT_MS = 5000;
@@ -216,15 +227,16 @@ const BLOB_MAX_BUFFER = MAX_CONFIG_BYTES + 1;
216
227
  /** The personal file, repository-relative, in git's spelling. */
217
228
  const PERSONAL_REL = '.devflow/config.json';
218
229
  /**
219
- * The spellings a case-folding file system opens as `.devflow` and `config.json`
220
- * beyond ASCII letter case. git's `icase` folds ASCII letters only, but APFS (the
221
- * macOS default) compares names by full Unicode case folding, so it also opens
222
- * `.devflow` (U+FB02 folds to "fl"), `config.json` (U+FB01 folds to "fi") and
223
- * `config.jſon` (U+017F folds to "s") at the personal path. A probe of every code
224
- * point on APFS finds no other one that folds into these names and none that it
225
- * ignores. The code is written with escapes, so each code point is named.
230
+ * The spellings a case-folding file system opens as `config.json` beyond ASCII letter
231
+ * case (the leaf lib's DEVFLOW_SPELLINGS lists those of `.devflow`). git's `icase` folds
232
+ * ASCII letters only, but APFS (the macOS default) compares names by full Unicode case
233
+ * folding, so it also opens `config.json` (U+FB01 folds to "fi") and `config.jſon`
234
+ * (U+017F folds to "s") at the personal path, as it opens `.devflow` (U+FB02 folds to
235
+ * "fl"). A probe of every code point on APFS finds no other one that folds into these
236
+ * names and none that it ignores. The code is written with escapes, so each code point
237
+ * is named.
226
238
  */
227
- const DEVFLOW_SPELLINGS = Object.freeze(['.devflow', '.dev\u{FB02}ow']);
239
+ const { DEVFLOW_SPELLINGS } = trackedAreas;
228
240
  const CONFIG_SPELLINGS = Object.freeze(['config.json', 'con\u{FB01}g.json', 'config.j\u{17F}on', 'con\u{FB01}g.j\u{17F}on']);
229
241
  /**
230
242
  * The pathspecs of the tracking check (D-PERSONAL-UNTRACKED), each matched
@@ -257,8 +269,10 @@ const PROJECT_REL = '.devflow/project.json';
257
269
 
258
270
  /**
259
271
  * @typedef {{ on: boolean, source: SwitchSource }} SwitchState
260
- * `source` is the layer that decided: `machine` when the machine switch is off
261
- * or nothing narrowed it, else the repo layer whose literal `false` narrowed it.
272
+ * `source` is the layer that decided: `machine` when the machine switch is off,
273
+ * when git tracks the area's folder or does not answer for it (a switch narrowed
274
+ * that way carries source `machine`, so it has no file to name), or when nothing
275
+ * narrowed it; else the repo layer whose literal `false` narrowed it.
262
276
  *
263
277
  * @typedef {{
264
278
  * ok: boolean,
@@ -340,8 +354,8 @@ const FAIL_CLOSED_SETTINGS = Object.freeze({
340
354
  reviewPublication: 'off',
341
355
  compliance: Object.freeze({ enabled: true, frameworks: Object.freeze([]) }),
342
356
  switches: Object.freeze({
343
- memory: Object.freeze({ on: true, source: /** @type {SwitchSource} */ ('machine') }),
344
- curation: Object.freeze({ on: true, source: /** @type {SwitchSource} */ ('machine') }),
357
+ memory: Object.freeze({ on: false, source: /** @type {SwitchSource} */ ('machine') }),
358
+ curation: Object.freeze({ on: false, source: /** @type {SwitchSource} */ ('machine') }),
345
359
  knowledge: Object.freeze({ on: false, source: /** @type {SwitchSource} */ ('machine') }),
346
360
  }),
347
361
  gateLevel: /** @type {GateLevel} */ ('full'),
@@ -705,7 +719,9 @@ function foldGateLevel(personal, manifest) {
705
719
  * The whole-file rule's resolution (D-SETTINGS-LINE): the fail-closed values,
706
720
  * naming the layer whose file could not be read, except the compliance lens —
707
721
  * a broken file affects only the keys it owns, so the lens stays the fold of
708
- * every readable layer (D-LENS-UNION). An unreadable project.json declares
722
+ * every readable layer (D-LENS-UNION) — and the memory and curation switches,
723
+ * which stay as the machine set them (an unreadable file narrows nothing, and
724
+ * the area answers narrow them afterwards). An unreadable project.json declares
709
725
  * nothing readable of its own (repoCompliance null) but reads as a malformed
710
726
  * declaration, so its lens is at least `generic`; an unreadable config.json
711
727
  * owns no compliance at all, so the worktree project.json's ids still count.
@@ -717,8 +733,11 @@ function foldGateLevel(personal, manifest) {
717
733
  function unreadableSettings(layer, inputs) {
718
734
  const defaultBranch = inputs.defaultBranch === undefined ? ABSENT_FIELD : inputs.defaultBranch;
719
735
  const lens = foldCompliance(inputs.project, inputs.manifest, defaultBranch);
736
+ /** @param {FeatureSwitch} feature @returns {SwitchState} */
737
+ const machine = feature => Object.freeze({ on: machineSwitchOn(inputs.manifest, feature), source: 'machine' });
720
738
  return Object.freeze({
721
739
  ...FAIL_CLOSED_SETTINGS,
740
+ switches: Object.freeze({ ...FAIL_CLOSED_SETTINGS.switches, memory: machine('memory'), curation: machine('curation') }),
722
741
  // The level stays `full` with no deciding layer, but a readable manifest still shows its own value.
723
742
  gateLevelMachine: machineGateLevel(inputs.manifest),
724
743
  compliance: lens.compliance,
@@ -758,6 +777,19 @@ function foldSettings(inputs) {
758
777
  });
759
778
  }
760
779
 
780
+ /**
781
+ * The settings for the layers as read: the whole-file rule's resolution for the first layer
782
+ * that could not be read (project before personal), else the fold of the layers.
783
+ *
784
+ * @param {SettingsInputs} inputs
785
+ * @returns {Settings}
786
+ */
787
+ function settingsOfLayers(inputs) {
788
+ if (inputs.project.kind === 'invalid') return unreadableSettings('project', inputs);
789
+ if (inputs.personal.kind === 'invalid') return unreadableSettings('personal', inputs);
790
+ return foldSettings(inputs);
791
+ }
792
+
761
793
  // ---------------------------------------------------------------------------
762
794
  // Gathering the layers (the imperative shell — D-SETTINGS-LOCAL-ONLY)
763
795
  // ---------------------------------------------------------------------------
@@ -773,18 +805,20 @@ function defaultExec(file, args, opts) {
773
805
 
774
806
  /**
775
807
  * One bounded local git call, normalized. The argv is a fresh copy, so an exec
776
- * cannot mutate a caller's constant.
808
+ * cannot mutate a caller's constant. `baseEnv` (default: the process environment)
809
+ * is the environment the call's own derives from.
777
810
  *
778
811
  * @param {ExecFn} exec
779
812
  * @param {string} cwd
780
813
  * @param {readonly string[]} args
781
814
  * @param {number} maxBuffer
815
+ * @param {NodeJS.ProcessEnv} [baseEnv]
782
816
  * @returns {GitAnswer}
783
817
  */
784
- function runGit(exec, cwd, args, maxBuffer) {
818
+ function runGit(exec, cwd, args, maxBuffer, baseEnv) {
785
819
  const res = exec('git', [...args], {
786
820
  cwd,
787
- env: Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' }),
821
+ env: Object.assign({}, baseEnv === undefined ? process.env : baseEnv, { GIT_TERMINAL_PROMPT: '0' }),
788
822
  stdio: ['ignore', 'pipe', 'pipe'],
789
823
  timeout: GIT_TIMEOUT_MS,
790
824
  maxBuffer,
@@ -849,7 +883,8 @@ function gitToplevel(exec, dir) {
849
883
  * so it is unreadable and its keys fail closed
850
884
  * `--no-literal-pathspecs` leads the argv: an inherited GIT_LITERAL_PATHSPECS=1
851
885
  * would read every pathspec as a plain path, magic included, list nothing and
852
- * read as untracked.
886
+ * read as untracked. The call runs without the variables that make git read
887
+ * another repository or index (the leaf lib's scrub), so it reads `root`'s own.
853
888
  *
854
889
  * @param {ExecFn} exec
855
890
  * @param {string} root
@@ -858,7 +893,8 @@ function gitToplevel(exec, dir) {
858
893
  */
859
894
  function personalTracking(exec, root, rootConfirmed) {
860
895
  const res = runGit(exec, root,
861
- ['--no-literal-pathspecs', '-c', 'core.fsmonitor=false', 'ls-files', '-z', '--', ...PERSONAL_TRACKING_PATHSPECS], LINE_MAX_BUFFER);
896
+ ['--no-literal-pathspecs', '-c', 'core.fsmonitor=false', 'ls-files', '-z', '--', ...PERSONAL_TRACKING_PATHSPECS], LINE_MAX_BUFFER,
897
+ trackedAreas.gitEnvWithoutRedirects(process.env));
862
898
  if (!res.answered) return 'unknown';
863
899
  if (res.stdout.length > 0) return 'tracked';
864
900
  if (res.ok) return 'untracked';
@@ -953,13 +989,180 @@ function defaultBranchCompliance(exec, root) {
953
989
  return fieldIn(projectConfig.parseProjectBytes(blob.stdout), 'compliance', 'malformed');
954
990
  }
955
991
 
992
+ // ---------------------------------------------------------------------------
993
+ // The tracked areas: memory at the toplevel, curation at the ledger root
994
+ // ---------------------------------------------------------------------------
995
+
996
+ /** What git before 2.31 prints first for `--path-format=absolute`: the flag it does not know, echoed back. */
997
+ const PATH_FORMAT_ECHO = '--path-format=absolute';
998
+
999
+ /**
1000
+ * How the `.git` entry at `root` stands, never followed: `folder`, `absent` (nothing there)
1001
+ * or `other` (a file, a link, or an entry that could not be probed).
1002
+ *
1003
+ * @param {string} root
1004
+ * @returns {'folder' | 'absent' | 'other'}
1005
+ */
1006
+ function gitEntryKind(root) {
1007
+ try {
1008
+ return fs.lstatSync(path.join(root, '.git')).isDirectory() ? 'folder' : 'other';
1009
+ } catch (err) {
1010
+ const code = err && /** @type {{ code?: string }} */ (err).code;
1011
+ return code === 'ENOENT' || code === 'ENOTDIR' ? 'absent' : 'other';
1012
+ }
1013
+ }
1014
+
1015
+ /**
1016
+ * Whether the main checkout `main` holds the ledger: it has a `.devflow` folder (links
1017
+ * followed), its own `.git` entry, and is not HOME (a repository rooted at HOME is the
1018
+ * machine root). The paths are compared physically, as the hooks' df_is_project_root does;
1019
+ * an unresolvable `main` holds none, and an unset or unresolvable HOME refuses nothing.
1020
+ *
1021
+ * @param {string} main
1022
+ * @returns {boolean}
1023
+ */
1024
+ function holdsLedger(main) {
1025
+ try {
1026
+ if (!fs.statSync(path.join(main, '.devflow')).isDirectory()) return false;
1027
+ fs.statSync(path.join(main, '.git'));
1028
+ const mainReal = fs.realpathSync(main);
1029
+ const home = process.env.HOME;
1030
+ if (typeof home !== 'string' || home === '') return true;
1031
+ try {
1032
+ return fs.realpathSync(home) !== mainReal;
1033
+ } catch (_) {
1034
+ return true;
1035
+ }
1036
+ } catch (_) {
1037
+ return false;
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Where the curation ledger lives for the checkout whose toplevel git named: the rule the
1043
+ * hooks apply (DF_LEDGER_ROOT of df_resolve_roots in hooks/resolve-project-root), which the
1044
+ * TypeScript twin getLedgerRoot (src/core/ledger-root.ts) shares.
1045
+ * - `<toplevel>/.git` is a folder, or there is none: the toplevel, with no git call. (A
1046
+ * main checkout is its own ledger root; with no entry the leaf lib asks no git call.)
1047
+ * - `<toplevel>/.git` is a file or a link: one `git rev-parse --path-format=absolute
1048
+ * --git-common-dir`. A path ending in `/.git` whose parent holds the ledger
1049
+ * (holdsLedger) names the parent; any other path names the toplevel. A git before 2.31
1050
+ * echoes the flag back as its first line: the toplevel, as the hooks fall back to it.
1051
+ * - any other answer, or none: `unknown` (the caller reads curation as unanswered).
1052
+ * Never throws.
1053
+ *
1054
+ * @param {string} toplevel an absolute path git named as a work tree's top level
1055
+ * @param {{ exec?: ExecFn }} [deps]
1056
+ * @returns {{ kind: 'root', root: string } | { kind: 'unknown' }}
1057
+ */
1058
+ function ledgerRootOf(toplevel, deps) {
1059
+ try {
1060
+ const exec = deps && typeof deps.exec === 'function' ? deps.exec : defaultExec;
1061
+ if (gitEntryKind(toplevel) !== 'other') return { kind: 'root', root: toplevel };
1062
+ const res = runGit(exec, toplevel, ['rev-parse', PATH_FORMAT_ECHO, '--git-common-dir'], LINE_MAX_BUFFER);
1063
+ if (!res.ok) return { kind: 'unknown' };
1064
+ const text = res.stdout.toString('utf8').replace(/\r?\n$/, '');
1065
+ if (text.split('\n', 1)[0] === PATH_FORMAT_ECHO) return { kind: 'root', root: toplevel };
1066
+ if (text === '' || /[\r\n\0]/.test(text) || !path.isAbsolute(text)) return { kind: 'unknown' };
1067
+ if (!text.endsWith('/.git')) return { kind: 'root', root: toplevel };
1068
+ const main = text.slice(0, -'/.git'.length);
1069
+ return { kind: 'root', root: main !== '' && holdsLedger(main) ? main : toplevel };
1070
+ } catch (_) {
1071
+ return { kind: 'unknown' };
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * @typedef {'tracked' | 'untracked' | 'unknown'} AreaAnswer
1077
+ * @typedef {{ memory: AreaAnswer, curation: AreaAnswer }} AreaAnswers
1078
+ */
1079
+
1080
+ /**
1081
+ * One area at one root, asked of the injected exec with the leaf lib's call (its pathspecs,
1082
+ * its scrub of redirecting variables, its bounds) and mapped by its judge. The leaf asks no
1083
+ * call, and answers `untracked`, for a root with no `.git` entry.
1084
+ *
1085
+ * @param {ExecFn} exec
1086
+ * @param {string} root
1087
+ * @param {'curation' | 'memory' | 'both'} area
1088
+ * @returns {AreaAnswer}
1089
+ */
1090
+ function askArea(exec, root, area) {
1091
+ const call = trackedAreas.trackedAreaCall(root, area);
1092
+ if (!('argv' in call)) return call.answer;
1093
+ const res = exec('git', [...call.argv], {
1094
+ cwd: call.cwd,
1095
+ env: call.env,
1096
+ stdio: ['ignore', 'pipe', 'ignore'],
1097
+ timeout: trackedAreas.TRACKED_AREAS_GIT_TIMEOUT_MS,
1098
+ maxBuffer: trackedAreas.TRACKED_AREAS_GIT_MAX_BUFFER,
1099
+ windowsHide: true,
1100
+ shell: false,
1101
+ });
1102
+ return trackedAreas.judgeTrackedAreas(res).answer;
1103
+ }
1104
+
1105
+ /**
1106
+ * The answers for the areas asked about. An area that is not asked about reads `untracked`
1107
+ * (nothing to narrow). Equal roots: ONE call over both areas; nothing listed ends it, git
1108
+ * not answering reads both `unknown`, and only a listing earns one call per area to tell
1109
+ * them apart. Different roots: one call per root. A curation root that is null (the ledger
1110
+ * root could not be named) reads `unknown`.
1111
+ *
1112
+ * @param {ExecFn} exec
1113
+ * @param {string} memoryRoot
1114
+ * @param {string | null} curationRoot
1115
+ * @param {{ memory: boolean, curation: boolean }} asked
1116
+ * @returns {AreaAnswers}
1117
+ */
1118
+ function trackedAreaAnswers(exec, memoryRoot, curationRoot, asked) {
1119
+ /** @type {AreaAnswers} */
1120
+ const answers = { memory: 'untracked', curation: 'untracked' };
1121
+ if (asked.memory && asked.curation && curationRoot === memoryRoot) {
1122
+ const union = askArea(exec, memoryRoot, 'both');
1123
+ if (union === 'untracked') return answers;
1124
+ if (union === 'unknown') return { memory: 'unknown', curation: 'unknown' };
1125
+ }
1126
+ if (asked.curation) answers.curation = curationRoot === null ? 'unknown' : askArea(exec, curationRoot, 'curation');
1127
+ if (asked.memory) answers.memory = askArea(exec, memoryRoot, 'memory');
1128
+ return answers;
1129
+ }
1130
+
1131
+ /**
1132
+ * The settings with each switch whose area is not `untracked` off. Only a switch still on
1133
+ * is narrowed, and it carries source `machine`: no file of the repository turned it off, so
1134
+ * there is none to name. Answers `settings` itself when nothing changes.
1135
+ *
1136
+ * @param {Settings} settings
1137
+ * @param {AreaAnswers} answers
1138
+ * @returns {Settings}
1139
+ */
1140
+ function narrowByAreas(settings, answers) {
1141
+ /** @param {'memory' | 'curation'} feature @returns {SwitchState} */
1142
+ const narrowed = feature => (answers[feature] === 'untracked' || !settings.switches[feature].on
1143
+ ? settings.switches[feature]
1144
+ : Object.freeze({ on: false, source: 'machine' }));
1145
+ const memory = narrowed('memory');
1146
+ const curation = narrowed('curation');
1147
+ if (memory === settings.switches.memory && curation === settings.switches.curation) return settings;
1148
+ return Object.freeze({ ...settings, switches: Object.freeze({ ...settings.switches, memory, curation }) });
1149
+ }
1150
+
956
1151
  /**
957
1152
  * Resolve the settings for `opts.dir`. Never throws: a git that cannot say
958
1153
  * whether `opts.dir` is in a repository, or any internal failure, is the
959
1154
  * fail-closed resolution, which main() maps to exit 4. A repository file that
960
- * exists but is unreadable fails every field closed but the compliance lens,
961
- * naming its layer in `unreadable` (the whole-file rule, D-SETTINGS-LINE);
962
- * main() prints that line with exit 0.
1155
+ * exists but is unreadable fails every field closed but the compliance lens and
1156
+ * the memory and curation switches, naming its layer in `unreadable` (the
1157
+ * whole-file rule, D-SETTINGS-LINE); main() prints that line with exit 0.
1158
+ *
1159
+ * Every repository return is then narrowed by the areas: memory at the toplevel,
1160
+ * curation at the ledger root, each off while git tracks its folders or does not
1161
+ * answer for them. When git answers that `opts.dir` is no repository (a lookup
1162
+ * that fails for a reason such as a redirecting variable or a refusal for
1163
+ * ownership), the leaf lib's check still runs at `opts.dir`: with no `.git`
1164
+ * entry there it asks no git call and changes nothing; with one it asks a scrubbed
1165
+ * call, and an unanswered call reads both areas as unknown.
963
1166
  *
964
1167
  * @param {ResolveSettingsOptions} opts
965
1168
  * @param {{ exec?: ExecFn }} [deps]
@@ -973,7 +1176,12 @@ function resolveSettings(opts, deps) {
973
1176
  if (toplevel.kind === 'unknown') return FAIL_CLOSED_SETTINGS;
974
1177
  if (toplevel.kind === 'none') {
975
1178
  const none = projectConfig.parseProjectBytes(null);
976
- return foldSettings({ project: none, personal: none, manifest, retiredPolicyFile: false });
1179
+ const folded = foldSettings({ project: none, personal: none, manifest, retiredPolicyFile: false });
1180
+ const start = path.resolve(opts.dir);
1181
+ return narrowByAreas(folded, trackedAreaAnswers(exec, start, start, {
1182
+ memory: folded.switches.memory.on,
1183
+ curation: folded.switches.curation.on,
1184
+ }));
977
1185
  }
978
1186
  const layers = readRepoLayers(toplevel.root, { exec, rootConfirmed: true });
979
1187
  /** @type {SettingsInputs} */
@@ -985,9 +1193,14 @@ function resolveSettings(opts, deps) {
985
1193
  defaultBranch: defaultBranchCompliance(exec, toplevel.root),
986
1194
  retiredPolicyFile: existsNoFollow(path.join(toplevel.root, '.devflow', 'policy.json')),
987
1195
  };
988
- if (layers.project.kind === 'invalid') return unreadableSettings('project', inputs);
989
- if (layers.personal.kind === 'invalid') return unreadableSettings('personal', inputs);
990
- return foldSettings(inputs);
1196
+ const folded = settingsOfLayers(inputs);
1197
+ const asked = { memory: folded.switches.memory.on, curation: folded.switches.curation.on };
1198
+ let curationRoot = null;
1199
+ if (asked.curation) {
1200
+ const ledger = ledgerRootOf(toplevel.root, { exec });
1201
+ curationRoot = ledger.kind === 'root' ? ledger.root : null;
1202
+ }
1203
+ return narrowByAreas(folded, trackedAreaAnswers(exec, toplevel.root, curationRoot, asked));
991
1204
  } catch (_) {
992
1205
  return FAIL_CLOSED_SETTINGS;
993
1206
  }
@@ -1238,6 +1451,7 @@ module.exports = Object.freeze({
1238
1451
  foldSettings,
1239
1452
  readRepoLayers,
1240
1453
  machineSwitchOn,
1454
+ ledgerRootOf,
1241
1455
  resolveSettings,
1242
1456
  formatSettingsLine,
1243
1457
  isCoherentSettingsLine,