@deftai/directive-core 0.67.0 → 0.68.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 (90) hide show
  1. package/dist/check/index.d.ts +1 -1
  2. package/dist/check/index.js +1 -1
  3. package/dist/check/orchestrator.d.ts +14 -4
  4. package/dist/check/orchestrator.js +30 -6
  5. package/dist/doctor/checks.js +11 -11
  6. package/dist/doctor/constants.d.ts +2 -0
  7. package/dist/doctor/constants.js +2 -0
  8. package/dist/doctor/payload-staleness.js +15 -6
  9. package/dist/forward-coverage/evaluate.d.ts +55 -0
  10. package/dist/forward-coverage/evaluate.js +254 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/init-deposit/constants.d.ts +14 -0
  14. package/dist/init-deposit/constants.js +10 -0
  15. package/dist/init-deposit/hygiene.d.ts +50 -0
  16. package/dist/init-deposit/hygiene.js +176 -0
  17. package/dist/init-deposit/init-deposit.d.ts +1 -0
  18. package/dist/init-deposit/init-deposit.js +7 -1
  19. package/dist/init-deposit/refresh.d.ts +2 -0
  20. package/dist/init-deposit/refresh.js +48 -30
  21. package/dist/init-deposit/scaffold.d.ts +3 -4
  22. package/dist/init-deposit/scaffold.js +3 -24
  23. package/dist/integration-e2e/helpers.d.ts +0 -1
  24. package/dist/integration-e2e/helpers.js +3 -7
  25. package/dist/layout/lifecycle-ref.d.ts +15 -0
  26. package/dist/layout/lifecycle-ref.js +55 -0
  27. package/dist/platform/agents-md.d.ts +28 -0
  28. package/dist/platform/agents-md.js +34 -0
  29. package/dist/policy/resolve.js +56 -46
  30. package/dist/pr-merge-readiness/compute.d.ts +10 -0
  31. package/dist/pr-merge-readiness/compute.js +53 -17
  32. package/dist/pr-merge-readiness/index.d.ts +2 -1
  33. package/dist/pr-merge-readiness/index.js +1 -0
  34. package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
  35. package/dist/pr-merge-readiness/mergeability.js +105 -0
  36. package/dist/pr-monitor/index.d.ts +1 -1
  37. package/dist/pr-monitor/index.js +1 -1
  38. package/dist/pr-monitor/monitor.d.ts +8 -2
  39. package/dist/pr-monitor/monitor.js +35 -6
  40. package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
  41. package/dist/pr-wait-mergeable/wrappers.js +13 -5
  42. package/dist/pr-watch/constants.d.ts +33 -0
  43. package/dist/pr-watch/constants.js +33 -0
  44. package/dist/pr-watch/index.d.ts +6 -0
  45. package/dist/pr-watch/index.js +6 -0
  46. package/dist/pr-watch/main.d.ts +21 -0
  47. package/dist/pr-watch/main.js +206 -0
  48. package/dist/pr-watch/probe.d.ts +13 -0
  49. package/dist/pr-watch/probe.js +100 -0
  50. package/dist/pr-watch/types.d.ts +47 -0
  51. package/dist/pr-watch/types.js +2 -0
  52. package/dist/pr-watch/watch.d.ts +12 -0
  53. package/dist/pr-watch/watch.js +122 -0
  54. package/dist/render/project-render.js +114 -89
  55. package/dist/scope/capacity-stamp.d.ts +24 -2
  56. package/dist/scope/capacity-stamp.js +80 -28
  57. package/dist/scope/decomposed-refs.js +2 -2
  58. package/dist/scope/project-definition-sync.js +3 -3
  59. package/dist/scope/vbrief-ref.d.ts +2 -1
  60. package/dist/scope/vbrief-ref.js +3 -2
  61. package/dist/spec-authority/constants.d.ts +6 -0
  62. package/dist/spec-authority/constants.js +11 -0
  63. package/dist/spec-authority/resolver.js +3 -3
  64. package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
  65. package/dist/swarm/finalize-cohort-cli.js +100 -0
  66. package/dist/swarm/finalize-cohort.d.ts +39 -0
  67. package/dist/swarm/finalize-cohort.js +589 -0
  68. package/dist/swarm/index.d.ts +2 -0
  69. package/dist/swarm/index.js +2 -0
  70. package/dist/swarm/launch.d.ts +7 -0
  71. package/dist/swarm/launch.js +17 -5
  72. package/dist/triage/queue/index.d.ts +1 -0
  73. package/dist/triage/queue/index.js +1 -0
  74. package/dist/triage/queue/project.d.ts +6 -1
  75. package/dist/triage/queue/project.js +9 -4
  76. package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
  77. package/dist/triage/queue/reconcile-live-state.js +54 -0
  78. package/dist/triage/scope/mutations-core.js +91 -81
  79. package/dist/triage/scope/resolve.js +4 -3
  80. package/dist/triage/scope-drift/add-ignore.js +33 -32
  81. package/dist/triage/scope-drift/scope-rules.js +3 -3
  82. package/dist/triage/subscribe/index.js +51 -45
  83. package/dist/triage/welcome/writers.js +79 -64
  84. package/dist/vbrief-build/project-definition-io.js +8 -3
  85. package/dist/vbrief-validate/paths.js +8 -4
  86. package/dist/vbrief-validate/precutover.d.ts +1 -1
  87. package/dist/vbrief-validate/precutover.js +12 -10
  88. package/package.json +7 -7
  89. package/dist/install-upgrade/index.d.ts +0 -13
  90. package/dist/install-upgrade/index.js +0 -251
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Scoped staging + installer-managed allowlist for TS-native init/update (#1453).
3
+ *
4
+ * Mirrors cmd/deft-install/hygiene.go + deposit.go installerManagedMatchers.
5
+ * Refs #1576, #1453, #1430.
6
+ */
7
+ import { type InitDepositIo } from "./constants.js";
8
+ export declare const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
9
+ export declare const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
10
+ export interface InstallerManagedMatcher {
11
+ readonly exact?: string;
12
+ readonly prefix?: string;
13
+ }
14
+ /** Single source of truth for installer-managed paths (#1440 / #1576). */
15
+ export declare function installerManagedMatchers(): InstallerManagedMatcher[];
16
+ /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
17
+ export declare function installerManagedGuardEre(): string;
18
+ export declare function isInstallerManagedPath(path: string): boolean;
19
+ export interface FrameworkStagePathsOptions {
20
+ /**
21
+ * Include the project-root ``Taskfile.yml`` in the stage set. Defaults to
22
+ * ``false``: unlike the other allowlisted paths, ``Taskfile.yml`` is a
23
+ * consumer-owned file that merely *contains* an installer-managed include
24
+ * block, so it must only be staged when the installer actually wired that
25
+ * block this run -- otherwise an unrelated user edit would be silently
26
+ * ``git add``ed (#1576 review).
27
+ */
28
+ readonly includeTaskfile?: boolean;
29
+ }
30
+ export declare function frameworkStagePaths(projectDir: string, deftDir: string, options?: FrameworkStagePathsOptions): string[];
31
+ export interface StageFrameworkPathsSeams {
32
+ gitPorcelain?: (projectRoot: string) => string | null;
33
+ runGitAdd?: (projectDir: string, paths: readonly string[]) => void;
34
+ }
35
+ /** Best-effort scoped `git add` — never fails the install/update (#1453 Layer 2b). */
36
+ export declare function stageFrameworkPaths(projectDir: string, paths: readonly string[], seams?: StageFrameworkPathsSeams): {
37
+ staged: boolean;
38
+ error: Error | null;
39
+ };
40
+ export declare const COMMIT_HYGIENE_BRANCH_NAME = "chore/deft-framework-upgrade";
41
+ export declare function printCommitGuidance(io: InitDepositIo, paths: readonly string[], staged: boolean): void;
42
+ export interface DepositStagePathsOptions extends StageFrameworkPathsSeams, FrameworkStagePathsOptions {
43
+ readCachedNames?: (projectDir: string) => string[];
44
+ }
45
+ export declare function depositStagePaths(projectDir: string, options?: DepositStagePathsOptions): {
46
+ stagePaths: string[];
47
+ staged: boolean;
48
+ stagedPaths: string[];
49
+ };
50
+ //# sourceMappingURL=hygiene.d.ts.map
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Scoped staging + installer-managed allowlist for TS-native init/update (#1453).
3
+ *
4
+ * Mirrors cmd/deft-install/hygiene.go + deposit.go installerManagedMatchers.
5
+ * Refs #1576, #1453, #1430.
6
+ */
7
+ import { execFileSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+ import { gitPorcelain } from "../story-ready/git.js";
11
+ import { CANONICAL_INSTALL_ROOT } from "./constants.js";
12
+ export const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
13
+ export const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
14
+ const VBRIEF_LIFECYCLE_DIRS = ["proposed", "pending", "active", "completed", "cancelled"];
15
+ /** Single source of truth for installer-managed paths (#1440 / #1576). */
16
+ export function installerManagedMatchers() {
17
+ return [
18
+ { exact: "AGENTS.md" },
19
+ { prefix: ".agents/" },
20
+ { prefix: ".githooks/" },
21
+ { exact: ".gitattributes" },
22
+ { exact: ".gitignore" },
23
+ { exact: "greptile.json" },
24
+ { exact: CODEQL_CONFIG_REL },
25
+ { exact: CORE_GUARD_WORKFLOW_REL },
26
+ { exact: "Taskfile.yml" },
27
+ { exact: "vbrief/.deft-version" },
28
+ { exact: "vbrief/vbrief.md" },
29
+ { prefix: "vbrief/schemas/" },
30
+ { prefix: "vbrief/migration/" },
31
+ ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `vbrief/${sub}/.gitkeep` })),
32
+ ];
33
+ }
34
+ function escapeEre(value) {
35
+ return value.replace(/[.^$*+?()[\]{}|\\]/g, "\\$&");
36
+ }
37
+ function matcherToEre(matcher) {
38
+ if (matcher.exact)
39
+ return `^${escapeEre(matcher.exact)}$`;
40
+ return `^${escapeEre(matcher.prefix ?? "")}`;
41
+ }
42
+ /** POSIX ERE alternation embedded in the deposited deft-core-guard workflow. */
43
+ export function installerManagedGuardEre() {
44
+ return installerManagedMatchers()
45
+ .map((matcher) => matcherToEre(matcher))
46
+ .join("|");
47
+ }
48
+ function matchesInstallerManaged(path, matchers) {
49
+ for (const matcher of matchers) {
50
+ if (matcher.exact && path === matcher.exact)
51
+ return true;
52
+ if (matcher.prefix && path.startsWith(matcher.prefix))
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+ export function isInstallerManagedPath(path) {
58
+ return matchesInstallerManaged(path, installerManagedMatchers());
59
+ }
60
+ export function frameworkStagePaths(projectDir, deftDir, options = {}) {
61
+ const paths = [];
62
+ const seen = new Set();
63
+ const add = (rel) => {
64
+ const normalized = rel.replace(/\\/g, "/");
65
+ if (!normalized || normalized === "." || seen.has(normalized))
66
+ return;
67
+ if (!existsSync(join(projectDir, normalized)))
68
+ return;
69
+ seen.add(normalized);
70
+ paths.push(normalized);
71
+ };
72
+ const relDeft = relative(projectDir, deftDir);
73
+ if (relDeft && !relDeft.startsWith("..") && !relDeft.startsWith("/")) {
74
+ add(relDeft);
75
+ }
76
+ for (const matcher of installerManagedMatchers()) {
77
+ if (matcher.exact === "Taskfile.yml" && !options.includeTaskfile)
78
+ continue;
79
+ if (matcher.exact) {
80
+ add(matcher.exact);
81
+ }
82
+ else if (matcher.prefix) {
83
+ add(matcher.prefix.replace(/\/$/, ""));
84
+ }
85
+ }
86
+ return paths;
87
+ }
88
+ /** Best-effort scoped `git add` — never fails the install/update (#1453 Layer 2b). */
89
+ export function stageFrameworkPaths(projectDir, paths, seams = {}) {
90
+ if (paths.length === 0)
91
+ return { staged: false, error: null };
92
+ const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
93
+ if (readPorcelain(projectDir) === null)
94
+ return { staged: false, error: null };
95
+ const runGitAdd = seams.runGitAdd ??
96
+ ((root, stagePaths) => {
97
+ execFileSync("git", ["add", ...stagePaths], {
98
+ cwd: root,
99
+ encoding: "utf8",
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+ });
103
+ try {
104
+ runGitAdd(projectDir, paths);
105
+ return { staged: true, error: null };
106
+ }
107
+ catch (cause) {
108
+ const error = cause instanceof Error ? cause : new Error(String(cause));
109
+ return { staged: false, error };
110
+ }
111
+ }
112
+ export const COMMIT_HYGIENE_BRANCH_NAME = "chore/deft-framework-upgrade";
113
+ export function printCommitGuidance(io, paths, staged) {
114
+ if (paths.length === 0)
115
+ return;
116
+ const addCmd = `git add ${paths.join(" ")}`;
117
+ io.printf("\nCommit hygiene (#1453, #1671): keep the framework deposit in its OWN branch/PR.\n");
118
+ io.printf("Do NOT use `git add -A` -- mixing the payload with your own files trips the\n");
119
+ io.printf("deft-core-guard CI check.\n");
120
+ if (staged) {
121
+ io.printf("The installer already staged ONLY these framework + installer-managed paths:\n");
122
+ io.printf(` ${addCmd}\n`);
123
+ }
124
+ else {
125
+ io.printf("Stage ONLY these framework + installer-managed paths:\n");
126
+ io.printf(` ${addCmd}\n`);
127
+ }
128
+ io.printf("Then take the framework deposit through the full PR lifecycle so deft-core-guard\n");
129
+ io.printf("evaluates a clean, standalone PR:\n");
130
+ io.printf(` 1. Branch: git switch -c ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
131
+ io.printf(' 2. Commit: git commit -m "chore(deft): update framework payload"\n');
132
+ io.printf(` 3. Push: git push -u origin ${COMMIT_HYGIENE_BRANCH_NAME}\n`);
133
+ io.printf(' 4. PR: gh pr create --fill --title "chore(deft): update framework payload"\n');
134
+ io.printf(" 5. Merge: gh pr merge --squash --delete-branch # after deft-core-guard passes\n");
135
+ }
136
+ function defaultCachedNames(projectDir) {
137
+ try {
138
+ const out = execFileSync("git", ["diff", "--cached", "--name-only", "-z"], {
139
+ cwd: projectDir,
140
+ encoding: "utf8",
141
+ stdio: ["ignore", "pipe", "pipe"],
142
+ });
143
+ return out
144
+ .split("\0")
145
+ .map((entry) => entry.trim().replace(/\\/g, "/"))
146
+ .filter(Boolean);
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ /**
153
+ * Narrow the candidate stage paths to those that actually have staged index
154
+ * changes. A candidate file matches when the cached name is identical; a
155
+ * candidate directory (e.g. ``.deft/core``) matches when any cached name lives
156
+ * beneath it. This keeps ``staged_paths`` honest for downstream automation --
157
+ * it reports what git actually staged, not every path passed to ``git add``.
158
+ */
159
+ function actuallyStagedPaths(stagePaths, cachedNames) {
160
+ return stagePaths.filter((candidate) => cachedNames.some((name) => name === candidate || name.startsWith(`${candidate}/`)));
161
+ }
162
+ export function depositStagePaths(projectDir, options = {}) {
163
+ const deftDir = join(projectDir, CANONICAL_INSTALL_ROOT);
164
+ const stagePaths = frameworkStagePaths(projectDir, deftDir, {
165
+ includeTaskfile: options.includeTaskfile ?? false,
166
+ });
167
+ const { staged } = stageFrameworkPaths(projectDir, stagePaths, options);
168
+ const readCachedNames = options.readCachedNames ?? defaultCachedNames;
169
+ const cachedNames = staged ? readCachedNames(projectDir) : [];
170
+ return {
171
+ stagePaths,
172
+ staged,
173
+ stagedPaths: staged ? actuallyStagedPaths(stagePaths, cachedNames) : [],
174
+ };
175
+ }
176
+ //# sourceMappingURL=hygiene.js.map
@@ -19,6 +19,7 @@ export interface InitDepositResult {
19
19
  readonly taskfileWired: boolean;
20
20
  readonly configDir: string;
21
21
  readonly legacyLayout: boolean;
22
+ readonly stagedPaths: string[];
22
23
  }
23
24
  export interface InitDepositSeams {
24
25
  resolveContentRoot?: () => Promise<string>;
@@ -13,6 +13,7 @@ import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
13
13
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
14
14
  import { readCorePackageVersion } from "../engine-version.js";
15
15
  import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
16
+ import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
16
17
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
17
18
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
18
19
  import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
@@ -93,7 +94,7 @@ export function buildInstallSummaryJson(result, options) {
93
94
  strategy: "vendor",
94
95
  dirty_tree: false,
95
96
  dirty_files: [],
96
- staged_paths: [],
97
+ staged_paths: result.stagedPaths,
97
98
  backup_path: "",
98
99
  previous_version: "",
99
100
  };
@@ -150,6 +151,10 @@ export async function runInitDeposit(args, io, seams = {}) {
150
151
  taskfileWired = ensureTaskfile(projectDir, io);
151
152
  }
152
153
  const configDir = createUserConfigDir(io);
154
+ const { stagePaths, staged, stagedPaths } = depositStagePaths(projectDir, {
155
+ includeTaskfile: taskfileWired,
156
+ });
157
+ printCommitGuidance(io, stagePaths, staged);
153
158
  return {
154
159
  projectDir,
155
160
  deftDir,
@@ -157,6 +162,7 @@ export async function runInitDeposit(args, io, seams = {}) {
157
162
  taskfileWired,
158
163
  configDir,
159
164
  legacyLayout: false,
165
+ stagedPaths,
160
166
  };
161
167
  }
162
168
  /** CLI-facing wrapper: runs deposit, emits JSON or wizard UX, returns exit code. */
@@ -22,6 +22,8 @@ export interface RefreshDepositResult {
22
22
  readonly agentsMdUpdated: boolean;
23
23
  readonly versionSkewNotice: string | null;
24
24
  readonly legacyLayout: boolean;
25
+ readonly taskfileWired: boolean;
26
+ readonly stagedPaths: string[];
25
27
  }
26
28
  export interface RefreshDepositSeams {
27
29
  resolveContentRoot?: () => Promise<string>;
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
11
  import { join, resolve } from "node:path";
12
12
  import { copyTree } from "../deposit/copy-tree.js";
13
13
  import { prunePythonArtifactsFromDeposit } from "../deposit/python-free.js";
@@ -17,31 +17,11 @@ import { readCorePackageVersion } from "../engine-version.js";
17
17
  import { resolveLifecycleRoot } from "../layout/resolve.js";
18
18
  import { DEV_FALLBACK } from "../platform/constants.js";
19
19
  import { gitPorcelain } from "../story-ready/git.js";
20
+ import { depositStagePaths, isInstallerManagedPath, printCommitGuidance } from "./hygiene.js";
20
21
  import { parseInitArgv } from "./init-deposit.js";
21
22
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
22
23
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
23
- import { CANONICAL_INSTALL_ROOT, depositNeutralization, writeAgentsMd, writeInstallManifest, } from "./scaffold.js";
24
- const INSTALLER_MANAGED_EXACT = new Set([
25
- "AGENTS.md",
26
- ".gitattributes",
27
- ".gitignore",
28
- "greptile.json",
29
- ".github/codeql/codeql-config.yml",
30
- ".github/workflows/deft-core-guard.yml",
31
- "vbrief/.deft-version",
32
- "vbrief/vbrief.md",
33
- "vbrief/proposed/.gitkeep",
34
- "vbrief/pending/.gitkeep",
35
- "vbrief/active/.gitkeep",
36
- "vbrief/completed/.gitkeep",
37
- "vbrief/cancelled/.gitkeep",
38
- ]);
39
- const INSTALLER_MANAGED_PREFIXES = [
40
- ".agents/",
41
- ".githooks/",
42
- "vbrief/schemas/",
43
- "vbrief/migration/",
44
- ];
24
+ import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeInstallManifest, } from "./scaffold.js";
45
25
  function normalizeVersion(version) {
46
26
  return version.trim().replace(/^v/, "");
47
27
  }
@@ -110,10 +90,34 @@ function syncBareVersionMarker(projectDir, version) {
110
90
  // best-effort, mirrors install-upgrade marker write
111
91
  }
112
92
  }
113
- function isInstallerManagedPath(path) {
114
- if (INSTALLER_MANAGED_EXACT.has(path))
115
- return true;
116
- return INSTALLER_MANAGED_PREFIXES.some((prefix) => path.startsWith(prefix));
93
+ /**
94
+ * Retire a stale legacy `.deft/VERSION` after the canonical `.deft/core/VERSION`
95
+ * has been (re)written (#2064). Folded in from the former `install-upgrade`
96
+ * path so the shared refresh transaction covers the v0.27.x -> v0.28 manifest
97
+ * transition (#1046 PR-B) that the doctor's `install-manifest-disagreement`
98
+ * check still cites `task upgrade` to repair. Only acts when the canonical
99
+ * manifest lives at `<project>/.deft/core/VERSION` and a legacy
100
+ * `<project>/.deft/VERSION` disagrees; renames the legacy file to
101
+ * `.deft/VERSION.premigrate` (best-effort, never fatal).
102
+ */
103
+ function migrateLegacyInstallManifest(projectDir, canonicalManifestPath) {
104
+ const canonical = resolve(canonicalManifestPath);
105
+ const expectedParent = resolve(projectDir, ".deft", "core");
106
+ if (resolve(canonical, "..") !== expectedParent)
107
+ return;
108
+ const legacy = join(projectDir, ".deft", "VERSION");
109
+ if (!existsSync(legacy) || !statSync(legacy).isFile())
110
+ return;
111
+ try {
112
+ const legacyVersion = manifestTagToVersion(parseInstallManifest(readFileSync(legacy, "utf8")));
113
+ const canonicalVersion = manifestTagToVersion(parseInstallManifest(readFileSync(canonical, "utf8")));
114
+ if (legacyVersion !== null && legacyVersion === canonicalVersion)
115
+ return;
116
+ renameSync(legacy, join(projectDir, ".deft", "VERSION.premigrate"));
117
+ }
118
+ catch {
119
+ // best-effort
120
+ }
117
121
  }
118
122
  function unquoteGitPath(path) {
119
123
  if (path.length >= 2 && path.startsWith('"') && path.endsWith('"')) {
@@ -206,7 +210,7 @@ export function buildUpdateSummaryJson(result, options) {
206
210
  update: true,
207
211
  non_interactive: options.nonInteractive,
208
212
  upgrade: options.upgrade,
209
- taskfile_wired: false,
213
+ taskfile_wired: result.taskfileWired,
210
214
  missing_tools: [],
211
215
  maintainer_mode: false,
212
216
  maintainer_tools: [],
@@ -217,7 +221,7 @@ export function buildUpdateSummaryJson(result, options) {
217
221
  strategy: "file-swap",
218
222
  dirty_tree: false,
219
223
  dirty_files: [],
220
- staged_paths: [],
224
+ staged_paths: result.stagedPaths,
221
225
  backup_path: "",
222
226
  previous_version: result.previousDepositVersion ?? "",
223
227
  content_version: result.contentVersion,
@@ -268,13 +272,25 @@ export async function runRefreshDeposit(args, io, seams = {}) {
268
272
  fetchedBy: "directive-update",
269
273
  ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
270
274
  };
271
- writeInstallManifest(projectDir, deftDir, manifestFields);
275
+ const writtenManifestPath = writeInstallManifest(projectDir, deftDir, manifestFields);
276
+ // #2064: retire a stale legacy .deft/VERSION now that the canonical
277
+ // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
278
+ // manifest behavior is lost by the redirect). Best-effort; never fatal.
279
+ migrateLegacyInstallManifest(projectDir, writtenManifestPath);
272
280
  // #2055: regenerate the bare .deft-version derivative so it agrees with the
273
281
  // freshly written manifest tag (otherwise doctor's manifest-agreement check
274
282
  // fails and the operator must hand-edit the marker).
275
283
  syncBareVersionMarker(projectDir, contentVersion);
276
284
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
277
285
  await depositNeutralization(projectDir, io);
286
+ let taskfileWired = false;
287
+ if (args.nonInteractive) {
288
+ taskfileWired = ensureTaskfile(projectDir, io);
289
+ }
290
+ const { stagePaths, staged, stagedPaths } = depositStagePaths(projectDir, {
291
+ includeTaskfile: taskfileWired,
292
+ });
293
+ printCommitGuidance(io, stagePaths, staged);
278
294
  const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
279
295
  printRefreshSideEffects(io, frameworkRefreshSideEffects(projectDir, readPorcelain));
280
296
  if (versionSkewNotice) {
@@ -289,6 +305,8 @@ export async function runRefreshDeposit(args, io, seams = {}) {
289
305
  agentsMdUpdated,
290
306
  versionSkewNotice,
291
307
  legacyLayout: false,
308
+ taskfileWired,
309
+ stagedPaths,
292
310
  };
293
311
  }
294
312
  /** CLI-facing wrapper: runs refresh, emits JSON or wizard UX, returns exit code. */
@@ -4,10 +4,9 @@
4
4
  *
5
5
  * Refs #1942, #1430, #1463, #1179.
6
6
  */
7
- export interface InitDepositIo {
8
- printf: (text: string) => void;
9
- }
10
- export declare const CANONICAL_INSTALL_ROOT = ".deft/core";
7
+ import { CANONICAL_INSTALL_ROOT, type InitDepositIo } from "./constants.js";
8
+ export type { InitDepositIo };
9
+ export { CANONICAL_INSTALL_ROOT };
11
10
  export declare const CORE_GLOB = ".deft/core/**";
12
11
  export declare const MINIMAL_TASKFILE = "version: '3'\n\n# Taskfile for this project.\n# Installed by deft-install --yes (Epic-4). Add your own tasks below or in\n# additional included files. The deft include makes all framework tasks\n# (task check, task vbrief:*, task doctor, etc.) available from the project root.\n\nincludes:\n deft:\n taskfile: ./.deft/core/Taskfile.yml\n optional: true\n";
13
12
  export declare const CANONICAL_TASKFILE_INCLUDE = "taskfile: ./.deft/core/Taskfile.yml";
@@ -12,7 +12,9 @@ import { dirname, join, relative } from "node:path";
12
12
  import { copyTree } from "../deposit/copy-tree.js";
13
13
  import { resolveLifecycleRoot } from "../layout/resolve.js";
14
14
  import { agentsRefreshPlan } from "../platform/agents-md.js";
15
- export const CANONICAL_INSTALL_ROOT = ".deft/core";
15
+ import { CANONICAL_INSTALL_ROOT } from "./constants.js";
16
+ import { installerManagedGuardEre } from "./hygiene.js";
17
+ export { CANONICAL_INSTALL_ROOT };
16
18
  export const CORE_GLOB = ".deft/core/**";
17
19
  const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
18
20
  const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
@@ -475,29 +477,6 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
475
477
  }
476
478
  return filesDeposited || configWired;
477
479
  }
478
- function escapeEre(value) {
479
- return value.replace(/[.^$*+?()[\]{}|\\]/g, "\\$&");
480
- }
481
- function installerManagedGuardEre() {
482
- const matchers = [
483
- { exact: "AGENTS.md" },
484
- { prefix: ".agents/" },
485
- { prefix: ".githooks/" },
486
- { exact: ".gitattributes" },
487
- { exact: ".gitignore" },
488
- { exact: "greptile.json" },
489
- { exact: CODEQL_CONFIG_REL },
490
- { exact: CORE_GUARD_WORKFLOW_REL },
491
- { exact: "vbrief/.deft-version" },
492
- { exact: "vbrief/vbrief.md" },
493
- { prefix: "vbrief/schemas/" },
494
- { prefix: "vbrief/migration/" },
495
- ...VBRIEF_LIFECYCLE_DIRS.map((sub) => ({ exact: `vbrief/${sub}/.gitkeep` })),
496
- ];
497
- return matchers
498
- .map((m) => (m.exact ? `^${escapeEre(m.exact)}$` : `^${escapeEre(m.prefix ?? "")}`))
499
- .join("|");
500
- }
501
480
  function githubActionsExpr(expression) {
502
481
  return ["$", "{{ ", expression, " }}"].join("");
503
482
  }
@@ -11,7 +11,6 @@ export interface DispatchCall {
11
11
  projectRoot: string;
12
12
  frameworkRoot: string;
13
13
  }
14
- export declare function isFrameworkSourceContext(frameworkRoot: string, projectRoot: string): boolean;
15
14
  /** Mirror ``scripts/_project_context.dispatch_task_check`` for consumer-task e2e. */
16
15
  export declare function dispatchTaskCheck(frameworkRoot: string, projectRoot: string, runner: (command: string, projectRoot: string, frameworkRoot: string) => {
17
16
  code: number;
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { join, resolve } from "node:path";
3
+ import { join } from "node:path";
4
4
  import { afterEach } from "vitest";
5
5
  import { restIssueListPaginated, setPaginatedLister, setSingleIssueFetcher, setSleepFn, } from "../cache/fetch.js";
6
6
  import { auditPath } from "../cache/paths.js";
@@ -108,14 +108,10 @@ export function writeScopeVbrief(projectRoot, folder, filename, status = "propos
108
108
  }, null, 2)}\n`, "utf8");
109
109
  return target;
110
110
  }
111
- export function isFrameworkSourceContext(frameworkRoot, projectRoot) {
112
- return resolve(frameworkRoot) === resolve(projectRoot);
113
- }
111
+ import { resolveCheckTarget } from "../check/orchestrator.js";
114
112
  /** Mirror ``scripts/_project_context.dispatch_task_check`` for consumer-task e2e. */
115
113
  export function dispatchTaskCheck(frameworkRoot, projectRoot, runner) {
116
- const target = isFrameworkSourceContext(frameworkRoot, projectRoot)
117
- ? "check:framework-source"
118
- : "check:consumer";
114
+ const target = resolveCheckTarget(frameworkRoot, projectRoot);
119
115
  return runner(target, projectRoot, frameworkRoot).code;
120
116
  }
121
117
  /** Hermetic mirror of ``tests/integration/test_scm_smoke._probe_rate_limit``. */
@@ -0,0 +1,15 @@
1
+ /** Lifecycle folders searched when a folder-qualified ref dangles (#1926). */
2
+ export declare const LIFECYCLE_SCOPE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
3
+ export interface ResolveLifecycleArtifactRefOptions {
4
+ /** When false, only resolve the literal path (for rewrite matching). Default true (#1926). */
5
+ readonly allowCrossFolderSearch?: boolean;
6
+ }
7
+ /**
8
+ * Resolve a lifecycle-relative artifact reference to an absolute path.
9
+ *
10
+ * When the direct path is missing but the ref names a lifecycle artifact
11
+ * (folder-qualified or bare basename), search all lifecycle folders for the
12
+ * same filename (#1926).
13
+ */
14
+ export declare function resolveLifecycleArtifactRef(rel: string, lifecycleRoot: string, options?: ResolveLifecycleArtifactRefOptions): string;
15
+ //# sourceMappingURL=lifecycle-ref.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { hasArtifactSuffix } from "./resolve.js";
4
+ /** Lifecycle folders searched when a folder-qualified ref dangles (#1926). */
5
+ export const LIFECYCLE_SCOPE_FOLDERS = [
6
+ "proposed",
7
+ "pending",
8
+ "active",
9
+ "completed",
10
+ "cancelled",
11
+ ];
12
+ function isLifecycleFolder(name) {
13
+ return LIFECYCLE_SCOPE_FOLDERS.includes(name);
14
+ }
15
+ function searchLifecycleFolders(lifecycleRoot, artifactBasename) {
16
+ for (const folder of LIFECYCLE_SCOPE_FOLDERS) {
17
+ const candidate = resolve(lifecycleRoot, folder, artifactBasename);
18
+ if (existsSync(candidate)) {
19
+ return candidate;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Resolve a lifecycle-relative artifact reference to an absolute path.
26
+ *
27
+ * When the direct path is missing but the ref names a lifecycle artifact
28
+ * (folder-qualified or bare basename), search all lifecycle folders for the
29
+ * same filename (#1926).
30
+ */
31
+ export function resolveLifecycleArtifactRef(rel, lifecycleRoot, options = {}) {
32
+ const allowCrossFolderSearch = options.allowCrossFolderSearch ?? true;
33
+ const direct = resolve(lifecycleRoot, rel);
34
+ if (existsSync(direct)) {
35
+ return direct;
36
+ }
37
+ if (!allowCrossFolderSearch) {
38
+ return direct;
39
+ }
40
+ const parts = rel.split("/").filter(Boolean);
41
+ if (parts.length >= 2 && isLifecycleFolder(parts[0] ?? "")) {
42
+ const found = searchLifecycleFolders(lifecycleRoot, basename(rel));
43
+ if (found !== null) {
44
+ return found;
45
+ }
46
+ }
47
+ if (!rel.includes("/") && hasArtifactSuffix(rel)) {
48
+ const found = searchLifecycleFolders(lifecycleRoot, rel);
49
+ if (found !== null) {
50
+ return found;
51
+ }
52
+ }
53
+ return direct;
54
+ }
55
+ //# sourceMappingURL=lifecycle-ref.js.map
@@ -1,3 +1,4 @@
1
+ import { type LockDeps } from "../slice/lock.js";
1
2
  export interface ManagedSectionAttrs {
2
3
  readonly version: number;
3
4
  readonly sha: string | null;
@@ -27,5 +28,32 @@ export declare function attributeRenderManagedSection(rendered: string, attrs: {
27
28
  }): string;
28
29
  /** Compute AGENTS.md managed-section freshness plan (mirrors scripts/_agents_md.py). */
29
30
  export declare function agentsRefreshPlan(projectRoot: string, seams?: AgentsMdSeams): Record<string, unknown>;
31
+ export interface ApplyAgentsRefreshOptions {
32
+ /** Compute state only, never write — mirrors `agents:refresh --check`. */
33
+ readonly check?: boolean;
34
+ /** Compute the plan but skip the write — mirrors `agents:refresh --dry-run`. */
35
+ readonly dryRun?: boolean;
36
+ }
37
+ export interface AgentsRefreshApplyResult {
38
+ readonly state: string;
39
+ readonly path: string;
40
+ /** True only when the managed section was written to disk on this call. */
41
+ readonly wrote: boolean;
42
+ /** True when the plan produced writable `new_content` for a writable state. */
43
+ readonly writable: boolean;
44
+ }
45
+ /**
46
+ * Serialize the AGENTS.md managed-section read->compute->write behind an advisory
47
+ * file lock and write atomically (temp file + rename), so concurrent
48
+ * install/upgrade/migrate/refresh writers cannot clobber one another's `session=`
49
+ * write or leave a partially-written file (#1329).
50
+ *
51
+ * Reuses the existing cross-process append lock (`withAppendLock`, keyed here on
52
+ * the AGENTS.md path -> `AGENTS.md.lock`) and atomic writer (`atomicWriteText`)
53
+ * rather than introducing a competing lock utility. The parallel
54
+ * `projectDefinitionMutationLock` remains owned by #1260; a general lock helper is
55
+ * deliberately not added here to avoid two competing mutation-lock utilities.
56
+ */
57
+ export declare function applyAgentsRefresh(projectRoot: string, options?: ApplyAgentsRefreshOptions, seams?: AgentsMdSeams, lockDeps?: LockDeps): AgentsRefreshApplyResult;
30
58
  export declare function hasV3ManagedMarker(projectRoot: string, readText?: (path: string) => string | null): boolean;
31
59
  //# sourceMappingURL=agents-md.d.ts.map