@deftai/directive-core 0.61.0 → 0.61.2

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.
@@ -14,6 +14,7 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
14
14
  import { readCorePackageVersion } from "../engine-version.js";
15
15
  import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./gitignore.js";
16
16
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
17
+ import { printMigrateNudgeIfNeeded } from "./migrate.js";
17
18
  import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
18
19
  export function parseInitArgv(canonicalArgv, userArgv = []) {
19
20
  const args = [...canonicalArgv, ...userArgv];
@@ -109,6 +110,7 @@ export function printNextSteps(result, io) {
109
110
  io.printf(" 2. Deft skill auto-discovery is partially implemented — if your agent doesn't\n");
110
111
  io.printf(' start setup automatically, tell it: "Use AGENTS.md"\n');
111
112
  io.printf(" 3. On first session, the agent will guide you through creating USER.md and PROJECT-DEFINITION.vbrief.json\n");
113
+ printMigrateNudgeIfNeeded(result.projectDir, io);
112
114
  io.printf("\n");
113
115
  }
114
116
  export async function runInitDeposit(args, io, seams = {}) {
@@ -85,4 +85,13 @@ export interface RunMigrateCliOptions {
85
85
  * stderr; config errors print to stderr; success prints to stdout.
86
86
  */
87
87
  export declare function runMigrateCli(options: RunMigrateCliOptions): number;
88
+ /** One-line nudge for init/update/session completion when provenance is unstamped (#2059). */
89
+ export declare const MIGRATE_COMPLETION_NUDGE = "[deft] One-time: run `directive migrate` to stamp npm provenance (idempotent). See content/UPGRADING.md.";
90
+ /** True when a canonical-vendored deposit exists and lacks the npm-managed sentinel. */
91
+ export declare function shouldEmitMigrateNudge(projectRoot: string, seams?: Pick<MigrateSeams, "isFile" | "readText">): boolean;
92
+ export interface MigrateNudgeIo {
93
+ printf: (text: string) => void;
94
+ }
95
+ /** Prints {@link MIGRATE_COMPLETION_NUDGE} when {@link shouldEmitMigrateNudge} is true. */
96
+ export declare function printMigrateNudgeIfNeeded(projectRoot: string, io: MigrateNudgeIo, seams?: Pick<MigrateSeams, "isFile" | "readText">): void;
88
97
  //# sourceMappingURL=migrate.d.ts.map
@@ -193,4 +193,27 @@ export function runMigrateCli(options) {
193
193
  }
194
194
  return result.exitCode;
195
195
  }
196
+ /** One-line nudge for init/update/session completion when provenance is unstamped (#2059). */
197
+ export const MIGRATE_COMPLETION_NUDGE = "[deft] One-time: run `directive migrate` to stamp npm provenance (idempotent). See content/UPGRADING.md.";
198
+ /** True when a canonical-vendored deposit exists and lacks the npm-managed sentinel. */
199
+ export function shouldEmitMigrateNudge(projectRoot, seams = {}) {
200
+ const isFile = seams.isFile ?? existsSync;
201
+ const readText = seams.readText ?? defaultReadText;
202
+ const manifestPath = detectCanonicalVendoredManifest(projectRoot, isFile);
203
+ if (manifestPath === null)
204
+ return false;
205
+ const text = readText(manifestPath);
206
+ if (text === null)
207
+ return false;
208
+ const manifest = parseInstallManifest(text);
209
+ if (Object.keys(manifest).length === 0)
210
+ return false;
211
+ return !isNpmManaged(manifest);
212
+ }
213
+ /** Prints {@link MIGRATE_COMPLETION_NUDGE} when {@link shouldEmitMigrateNudge} is true. */
214
+ export function printMigrateNudgeIfNeeded(projectRoot, io, seams = {}) {
215
+ if (shouldEmitMigrateNudge(projectRoot, seams)) {
216
+ io.printf(`\n${MIGRATE_COMPLETION_NUDGE}\n`);
217
+ }
218
+ }
196
219
  //# sourceMappingURL=migrate.js.map
@@ -7,16 +7,18 @@
7
7
  *
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
- import { existsSync, readFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, 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";
14
14
  import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
15
15
  import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
16
16
  import { readCorePackageVersion } from "../engine-version.js";
17
+ import { DEV_FALLBACK } from "../platform/constants.js";
17
18
  import { gitPorcelain } from "../story-ready/git.js";
18
19
  import { parseInitArgv } from "./init-deposit.js";
19
20
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
21
+ import { printMigrateNudgeIfNeeded } from "./migrate.js";
20
22
  import { CANONICAL_INSTALL_ROOT, depositNeutralization, writeAgentsMd, writeInstallManifest, } from "./scaffold.js";
21
23
  const INSTALLER_MANAGED_EXACT = new Set([
22
24
  "AGENTS.md",
@@ -67,6 +69,46 @@ function readRecordedDepositVersion(deftDir) {
67
69
  return null;
68
70
  }
69
71
  }
72
+ /** Prior `managed_by` provenance sentinel from the deposit manifest, if any (#2056). */
73
+ function readRecordedManagedBy(deftDir) {
74
+ const manifestPath = join(deftDir, "VERSION");
75
+ if (!existsSync(manifestPath))
76
+ return null;
77
+ try {
78
+ const value = (parseInstallManifest(readFileSync(manifestPath, "utf8")).managed_by ?? "").trim();
79
+ return value || null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * Regenerate the bare `.deft-version` derivative from the deposited content
87
+ * version in the same transaction as the payload swap (#2055). The canonical
88
+ * marker lives at `vbrief/.deft-version`; fall back to the project root only
89
+ * when `vbrief/` is absent. Never persist the dev fallback.
90
+ */
91
+ function syncBareVersionMarker(projectDir, version) {
92
+ const normalized = normalizeVersion(version);
93
+ if (!normalized || normalized === DEV_FALLBACK)
94
+ return;
95
+ const vbriefDir = join(projectDir, "vbrief");
96
+ let targetDir = projectDir;
97
+ try {
98
+ if (statSync(vbriefDir).isDirectory())
99
+ targetDir = vbriefDir;
100
+ }
101
+ catch {
102
+ // vbrief/ absent — write the root-level derivative instead
103
+ }
104
+ try {
105
+ mkdirSync(targetDir, { recursive: true });
106
+ writeFileSync(join(targetDir, ".deft-version"), `${normalized}\n`, "utf8");
107
+ }
108
+ catch {
109
+ // best-effort, mirrors install-upgrade marker write
110
+ }
111
+ }
70
112
  function isInstallerManagedPath(path) {
71
113
  if (INSTALLER_MANAGED_EXACT.has(path))
72
114
  return true;
@@ -190,6 +232,7 @@ export function printUpdateComplete(result, io) {
190
232
  if (result.versionSkewNotice) {
191
233
  io.printf(`\n${result.versionSkewNotice}\n`);
192
234
  }
235
+ printMigrateNudgeIfNeeded(result.projectDir, io);
193
236
  io.printf("\n");
194
237
  }
195
238
  export async function runRefreshDeposit(args, io, seams = {}) {
@@ -208,6 +251,7 @@ export async function runRefreshDeposit(args, io, seams = {}) {
208
251
  const readPackageVersion = seams.readPackageVersion ?? readCorePackageVersion;
209
252
  const contentRoot = await resolveContent();
210
253
  const previousDepositVersion = readRecordedDepositVersion(deftDir);
254
+ const previousManagedBy = readRecordedManagedBy(deftDir);
211
255
  const engineVersion = readEngine();
212
256
  const contentVersion = readContentPackageVersion(contentRoot, readPackageVersion);
213
257
  const versionSkewNotice = buildVersionSkewNotice(engineVersion, contentVersion, previousDepositVersion);
@@ -221,8 +265,13 @@ export async function runRefreshDeposit(args, io, seams = {}) {
221
265
  installRoot: CANONICAL_INSTALL_ROOT,
222
266
  fetchedAt: nowIso(),
223
267
  fetchedBy: "directive-update",
268
+ ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
224
269
  };
225
270
  writeInstallManifest(projectDir, deftDir, manifestFields);
271
+ // #2055: regenerate the bare .deft-version derivative so it agrees with the
272
+ // freshly written manifest tag (otherwise doctor's manifest-agreement check
273
+ // fails and the operator must hand-edit the marker).
274
+ syncBareVersionMarker(projectDir, contentVersion);
226
275
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
227
276
  await depositNeutralization(projectDir, io);
228
277
  const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
@@ -18,6 +18,13 @@ export interface InstallManifestFields {
18
18
  installRoot: string;
19
19
  fetchedAt: string;
20
20
  fetchedBy: string;
21
+ /**
22
+ * Provenance sentinel (#2056). When the prior manifest was stamped
23
+ * `managed_by: 'npm'` by the npm-migration path, callers thread it through a
24
+ * manifest rebuild so a routine `directive update` / `install-upgrade` does not
25
+ * regress provenance and re-arm the doctor signpost.
26
+ */
27
+ managedBy?: string;
21
28
  }
22
29
  export declare function buildInstallManifestText(fields: InstallManifestFields): string;
23
30
  export declare function writeInstallManifest(projectDir: string, deftDir: string, fields: InstallManifestFields): string;
@@ -187,12 +187,17 @@ export function buildInstallManifestText(fields) {
187
187
  effectiveTag = `v${effectiveTag}`;
188
188
  }
189
189
  const effectiveRef = fields.ref || effectiveTag;
190
- return (`ref: '${effectiveRef}'\n` +
190
+ let body = `ref: '${effectiveRef}'\n` +
191
191
  `sha: '${fields.sha}'\n` +
192
192
  `tag: '${effectiveTag}'\n` +
193
193
  `install_root: '${fields.installRoot}'\n` +
194
194
  `fetched_at: '${fields.fetchedAt}'\n` +
195
- `fetched_by: '${fields.fetchedBy}'\n`);
195
+ `fetched_by: '${fields.fetchedBy}'\n`;
196
+ const managedBy = fields.managedBy?.trim();
197
+ if (managedBy) {
198
+ body += `managed_by: '${managedBy}'\n`;
199
+ }
200
+ return body;
196
201
  }
197
202
  export function writeInstallManifest(projectDir, deftDir, fields) {
198
203
  const installRoot = fields.installRoot ||
@@ -1,11 +1,75 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
- import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
3
+ import { locateManifest, manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
4
+ import { readCorePackageVersion } from "../engine-version.js";
4
5
  import { buildInstallManifestText, CANONICAL_INSTALL_ROOT, } from "../init-deposit/scaffold.js";
5
6
  import { agentsRefreshPlan } from "../platform/agents-md.js";
6
7
  import { DEV_FALLBACK } from "../platform/constants.js";
7
8
  import { resolveVersion } from "../platform/resolve-version.js";
8
9
  import { detectPreCutoverLegacy } from "../vbrief-validate/precutover.js";
10
+ const ENGINE_PACKAGE_FALLBACK = "0.0.0";
11
+ /**
12
+ * Resolve the framework version for an upgrade (#2053).
13
+ *
14
+ * `resolveVersion({ frameworkRoot })` reads env / install-manifest / .deft-version
15
+ * / git against the *engine* framework root. When `deft install-upgrade` runs from
16
+ * a global npm install, that root is the npm package directory — which carries no
17
+ * install manifest, no bare marker, and is not its own git repo — so the chain
18
+ * dead-ends at `0.0.0-dev`. Recover by auto-detecting the *consumer project's*
19
+ * deposited manifest, then fall back to the engine package.json version, before
20
+ * surrendering to the dev fallback.
21
+ */
22
+ function resolveUpgradeVersion(frameworkRoot, projectRoot) {
23
+ const primary = resolveVersion({ frameworkRoot });
24
+ if (primary !== DEV_FALLBACK)
25
+ return primary;
26
+ const manifestPath = locateManifest(projectRoot, null);
27
+ if (manifestPath) {
28
+ try {
29
+ const tag = manifestTagToVersion(parseInstallManifest(readFileSync(manifestPath, "utf8")));
30
+ if (tag)
31
+ return tag;
32
+ }
33
+ catch {
34
+ // fall through to package.json fallback
35
+ }
36
+ }
37
+ const pkgVersion = readCorePackageVersion();
38
+ if (pkgVersion && pkgVersion !== ENGINE_PACKAGE_FALLBACK)
39
+ return pkgVersion;
40
+ return DEV_FALLBACK;
41
+ }
42
+ /**
43
+ * The consumer's deposited framework install root (`.deft/core`, then legacy
44
+ * `deft/`), if present. Used as the AGENTS.md render root and manifest target so
45
+ * the refresh renders from the *deposited* templates that match the installed
46
+ * payload -- not the (possibly newer) templates bundled in a global npm engine.
47
+ */
48
+ function resolveInstallRoot(projectRoot) {
49
+ for (const candidate of [join(projectRoot, ".deft", "core"), join(projectRoot, "deft")]) {
50
+ try {
51
+ if (statSync(candidate).isDirectory())
52
+ return candidate;
53
+ }
54
+ catch {
55
+ // try next candidate
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ /** Prior `managed_by` provenance sentinel from an install manifest, if any (#2056). */
61
+ function readManagedByAt(installRoot) {
62
+ const manifestPath = join(installRoot, "VERSION");
63
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile())
64
+ return null;
65
+ try {
66
+ const value = (parseInstallManifest(readFileSync(manifestPath, "utf8")).managed_by ?? "").trim();
67
+ return value || null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
9
73
  function versionMarkerPaths(projectRoot) {
10
74
  return [join(projectRoot, "vbrief", ".deft-version"), join(projectRoot, ".deft-version")];
11
75
  }
@@ -46,6 +110,7 @@ function deriveInstallRootString(installRoot, projectRoot) {
46
110
  function writeInstallManifestAt(installRoot, projectRoot, version) {
47
111
  if (version.replace(/^v/, "") === DEV_FALLBACK)
48
112
  return null;
113
+ const priorManagedBy = readManagedByAt(installRoot);
49
114
  const fields = {
50
115
  ref: version.startsWith("v") ? version : `v${version}`,
51
116
  sha: "content-package",
@@ -53,6 +118,7 @@ function writeInstallManifestAt(installRoot, projectRoot, version) {
53
118
  installRoot: deriveInstallRootString(installRoot, projectRoot),
54
119
  fetchedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
55
120
  fetchedBy: "deft-upgrade",
121
+ ...(priorManagedBy ? { managedBy: priorManagedBy } : {}),
56
122
  };
57
123
  try {
58
124
  mkdirSync(installRoot, { recursive: true });
@@ -111,13 +177,19 @@ function runAgentsRefresh(projectRoot, frameworkRoot, io) {
111
177
  export function runInstallUpgrade(args, io) {
112
178
  const projectRoot = resolve(args.projectRoot);
113
179
  const frameworkRoot = resolve(args.frameworkRoot);
114
- const version = resolveVersion({ frameworkRoot });
180
+ const version = resolveUpgradeVersion(frameworkRoot, projectRoot);
115
181
  const normalizedVersion = version.startsWith("v") ? version.slice(1) : version;
182
+ // Render AGENTS.md from the deposited install root so the managed section
183
+ // matches the installed payload, not the engine's bundled templates (which a
184
+ // global npm `deft` may carry at a different version). Falls back to the engine
185
+ // framework root when no deposit is present.
186
+ const installRoot = resolveInstallRoot(projectRoot);
187
+ const agentsRoot = installRoot ?? frameworkRoot;
116
188
  io.writeOut(`Deft CLI v${normalizedVersion} - Upgrade\n\n`);
117
189
  const recorded = readVersionMarker(projectRoot);
118
190
  if (recorded === normalizedVersion) {
119
191
  io.writeOut(`Project already at ${normalizedVersion}. Nothing to do.\n`);
120
- return runAgentsRefresh(projectRoot, frameworkRoot, io);
192
+ return runAgentsRefresh(projectRoot, agentsRoot, io);
121
193
  }
122
194
  const legacy = detectPreCutoverLegacy(projectRoot);
123
195
  if (legacy.length > 0) {
@@ -126,21 +198,25 @@ export function runInstallUpgrade(args, io) {
126
198
  const vbriefDir = join(projectRoot, "vbrief");
127
199
  const targetDir = existsSync(vbriefDir) && statSync(vbriefDir).isDirectory() ? vbriefDir : projectRoot;
128
200
  writeVersionMarker(targetDir, normalizedVersion);
129
- let writtenManifestPath = null;
130
- for (const installCandidate of [join(projectRoot, ".deft", "core"), join(projectRoot, "deft")]) {
131
- if (existsSync(installCandidate) && statSync(installCandidate).isDirectory()) {
132
- writtenManifestPath = writeInstallManifestAt(installCandidate, projectRoot, normalizedVersion);
133
- break;
134
- }
135
- }
201
+ const writtenManifestPath = installRoot !== null
202
+ ? writeInstallManifestAt(installRoot, projectRoot, normalizedVersion)
203
+ : null;
136
204
  migrateLegacyInstallManifest(projectRoot, writtenManifestPath);
137
- if (recorded === null) {
205
+ if (normalizedVersion === DEV_FALLBACK) {
206
+ // #2053: the marker + manifest writers above no-op on the dev fallback, so do
207
+ // not claim a marker update that never happened.
208
+ io.writeOut("Could not resolve a published framework version (resolved 0.0.0-dev); " +
209
+ ".deft-version marker and install manifest were left unchanged. On a consumer " +
210
+ "install, ensure <project>/.deft/core/VERSION carries a real tag, or upgrade the " +
211
+ "engine with `npm i -g @deftai/directive@latest`.\n");
212
+ }
213
+ else if (recorded === null) {
138
214
  io.writeOut(`Recorded framework version ${normalizedVersion} in .deft-version.\n`);
139
215
  }
140
216
  else {
141
217
  io.writeOut(`Updated .deft-version from ${recorded} to ${normalizedVersion}.\n`);
142
218
  }
143
219
  io.writeOut("If legacy SPECIFICATION.md or PROJECT.md content remains, run `task migrate:vbrief` to complete the upgrade.\n");
144
- return runAgentsRefresh(projectRoot, frameworkRoot, io);
220
+ return runAgentsRefresh(projectRoot, agentsRoot, io);
145
221
  }
146
222
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { runningInsideDeftRepo } from "../doctor/paths.js";
3
+ import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposit/migrate.js";
2
4
  import { disclosureLine } from "../policy/disclosure.js";
3
5
  import { resolvePolicy } from "../policy/resolve.js";
4
6
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
@@ -263,6 +265,9 @@ export function runSessionStart(projectRoot, options = {}) {
263
265
  lines.push(message);
264
266
  }
265
267
  }
268
+ if (!runningInsideDeftRepo(projectRoot) && shouldEmitMigrateNudge(projectRoot)) {
269
+ lines.push(MIGRATE_COMPLETION_NUDGE);
270
+ }
266
271
  const payload = newRitualStatePayload({
267
272
  sessionId: (options.newSessionId ?? randomUUID)(),
268
273
  gitHead: gitHeadValue,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.61.0",
3
+ "version": "0.61.2",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -221,8 +221,8 @@
221
221
  "provenance": true
222
222
  },
223
223
  "dependencies": {
224
- "@deftai/directive-content": "^0.61.0",
225
- "@deftai/directive-types": "^0.61.0"
224
+ "@deftai/directive-content": "^0.61.2",
225
+ "@deftai/directive-types": "^0.61.2"
226
226
  },
227
227
  "scripts": {
228
228
  "build": "tsc -b",