@deftai/directive-core 0.84.0 → 0.86.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 (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -23,11 +23,13 @@
23
23
  *
24
24
  * Refs #1912, #1669.
25
25
  */
26
- import { statSync } from "node:fs";
26
+ import { execFileSync } from "node:child_process";
27
+ import { rmSync, statSync } from "node:fs";
27
28
  import { join } from "node:path";
28
29
  import { GO_BRIDGE_RELEASES_URL, UPGRADING_DOC_URL } from "../doctor/constants.js";
29
30
  import { extractManagedSection, parseInstallRootFromAgentsMd } from "../doctor/manifest.js";
30
31
  import { readTextSafe } from "../doctor/paths.js";
32
+ import { gitPorcelain } from "../story-ready/git.js";
31
33
  /** Non-zero exit code for a use-time legacy-layout refusal (needs-action). */
32
34
  export const LEGACY_LAYOUT_REFUSED_EXIT_CODE = 2;
33
35
  const NOT_LEGACY = {
@@ -53,6 +55,16 @@ function defaultIsDir(p) {
53
55
  }
54
56
  }
55
57
  /** Does `.gitmodules` reference the deft framework at a deft install path? */
58
+ const LEGACY_DEFT_DIR = "deft";
59
+ function isFrameworkDeftDir(projectDir, isDir, isFile) {
60
+ const deftDir = join(projectDir, LEGACY_DEFT_DIR);
61
+ const deftMarkers = [
62
+ join(deftDir, "VERSION"),
63
+ join(deftDir, "main.md"),
64
+ join(deftDir, "Taskfile.yml"),
65
+ ];
66
+ return isDir(deftDir) && (deftMarkers.some((m) => isFile(m)) || isDir(join(deftDir, "skills")));
67
+ }
56
68
  function gitmodulesReferencesFramework(text) {
57
69
  const normalized = text.replace(/\r\n/g, "\n").toLowerCase();
58
70
  if (normalized.includes("deftai/directive")) {
@@ -62,6 +74,27 @@ function gitmodulesReferencesFramework(text) {
62
74
  // when the url host differs (mirror, fork). Match `path = deft` / `path = .deft`.
63
75
  return /(^|\n)\s*path\s*=\s*\.?deft(\/|\s|$)/.test(normalized);
64
76
  }
77
+ const DUAL_LAYOUT_DETAIL = "Found both the canonical .deft/core/ deposit and a leftover legacy deft/ " +
78
+ "framework tree -- remove deft/ so .deft/core/ is the sole framework root.";
79
+ /**
80
+ * Detect a post-bridge dual-layout: canonical `.deft/core/` plus a leftover
81
+ * framework-marked `deft/` tree. Doctor-only -- the npm deposit path treats
82
+ * `.deft/core/` as canonical and must not refuse here (#2805 / #2804 wiring).
83
+ */
84
+ export function detectDualLayout(projectDir, seams = {}) {
85
+ const isFile = seams.isFile ?? defaultIsFile;
86
+ const isDir = seams.isDir ?? defaultIsDir;
87
+ const hasCanonicalCore = isDir(join(projectDir, ".deft", "core"));
88
+ if (!hasCanonicalCore || !isFrameworkDeftDir(projectDir, isDir, isFile)) {
89
+ return null;
90
+ }
91
+ return {
92
+ legacy: true,
93
+ kind: "dual-layout",
94
+ detail: DUAL_LAYOUT_DETAIL,
95
+ evidence: [".deft/core/", "deft/"],
96
+ };
97
+ }
65
98
  /**
66
99
  * Classify the on-disk layout at `projectDir`. Returns `legacy: false` for the
67
100
  * canonical `.deft/core/` vendored layout and for a greenfield project (so init
@@ -71,10 +104,11 @@ export function detectLegacyLayout(projectDir, seams = {}) {
71
104
  const isFile = seams.isFile ?? defaultIsFile;
72
105
  const isDir = seams.isDir ?? defaultIsDir;
73
106
  const readText = seams.readText ?? readTextSafe;
107
+ const hasCanonicalCore = isDir(join(projectDir, ".deft", "core"));
108
+ const deftDirIsFramework = isFrameworkDeftDir(projectDir, isDir, isFile);
74
109
  // Canonical vendored layout present -> the npm path owns it; not legacy. Any
75
- // AGENTS.md / manifest drift here is repaired by the healthy refresh path,
76
- // not by refusing.
77
- if (isDir(join(projectDir, ".deft", "core"))) {
110
+ // leftover deft/ tree is a dual-layout hygiene issue surfaced by Doctor only.
111
+ if (hasCanonicalCore) {
78
112
  return NOT_LEGACY;
79
113
  }
80
114
  // Orphan `.deft/VERSION` (the old pre-`.deft/core/` manifest location) with no
@@ -92,13 +126,7 @@ export function detectLegacyLayout(projectDir, seams = {}) {
92
126
  // framework markers. Distinguish a git-clone / submodule deposit (a `.git`
93
127
  // inside `deft/`, or a `.gitmodules` pointing at the framework) from a plain
94
128
  // legacy-prefixed vendored copy.
95
- const deftDir = join(projectDir, "deft");
96
- const deftMarkers = [
97
- join(deftDir, "VERSION"),
98
- join(deftDir, "main.md"),
99
- join(deftDir, "Taskfile.yml"),
100
- ];
101
- const deftDirIsFramework = isDir(deftDir) && (deftMarkers.some((m) => isFile(m)) || isDir(join(deftDir, "skills")));
129
+ const deftDir = join(projectDir, LEGACY_DEFT_DIR);
102
130
  if (deftDirIsFramework) {
103
131
  const submoduleGit = isFile(join(deftDir, ".git")) || isDir(join(deftDir, ".git"));
104
132
  if (submoduleGit) {
@@ -215,9 +243,113 @@ export function buildLegacyRefusalJson(command, projectDir, detection) {
215
243
  }
216
244
  /** One-line doctor signpost carrying the stable URL (no baked version/command). */
217
245
  export function legacyLayoutSignpostLine(detection) {
246
+ if (detection.kind === "dual-layout") {
247
+ return dualLayoutSignpostLine(detection);
248
+ }
218
249
  return (`Legacy Deft layout detected (${detection.kind ?? "unknown"}): ${detection.detail} ` +
219
250
  "Run the frozen Go bridge installer to migrate to .deft/core/, then use the npm " +
220
251
  `CLI (\`npx @deftai/directive update\`). See ${UPGRADING_DOC_URL} ` +
221
252
  `(frozen bridge: ${GO_BRIDGE_RELEASES_URL}).`);
222
253
  }
254
+ /** Doctor / npm signpost for the post-bridge dual-layout state (#2805). */
255
+ export function dualLayoutSignpostLine(detection) {
256
+ return (`Dual Deft layout detected (${detection.kind ?? "dual-layout"}): ${detection.detail} ` +
257
+ "Remove the legacy deft/ tree so .deft/core/ is the sole framework root " +
258
+ "(when clean: `git rm -r deft/`; reconcile local edits first if `git status deft/` is dirty). " +
259
+ `See ${UPGRADING_DOC_URL}.`);
260
+ }
261
+ function porcelainEntryPath(line) {
262
+ let path = line.slice(3).trim().replace(/\\/g, "/");
263
+ if (path.startsWith('"') && path.endsWith('"')) {
264
+ path = path.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
265
+ }
266
+ return path;
267
+ }
268
+ function porcelainTouchesLegacyDeft(porcelain) {
269
+ const touched = [];
270
+ for (const line of porcelain.split("\n")) {
271
+ if (!line.trim())
272
+ continue;
273
+ const path = porcelainEntryPath(line);
274
+ if (path === LEGACY_DEFT_DIR || path.startsWith(`${LEGACY_DEFT_DIR}/`)) {
275
+ touched.push(path);
276
+ }
277
+ }
278
+ return touched;
279
+ }
280
+ /**
281
+ * Remove a clean tracked legacy `deft/` tree after bridge migration (#2805).
282
+ * Refuses when the tree has local modifications or untracked files.
283
+ */
284
+ export function tryCleanupLegacyDeftTree(projectDir, seams = {}) {
285
+ const detection = detectDualLayout(projectDir);
286
+ if (detection === null) {
287
+ return {
288
+ ok: true,
289
+ action: "skipped",
290
+ detail: "No dual-layout legacy deft/ tree to remove.",
291
+ };
292
+ }
293
+ const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
294
+ const porcelain = readPorcelain(projectDir);
295
+ if (porcelain === null) {
296
+ return {
297
+ ok: false,
298
+ action: "refused",
299
+ detail: "Cannot determine whether the legacy deft/ tree is clean (git unavailable). " +
300
+ "Inspect `git status deft/` and remove the tree manually once clean.",
301
+ };
302
+ }
303
+ const dirtyPaths = porcelainTouchesLegacyDeft(porcelain);
304
+ if (dirtyPaths.length > 0) {
305
+ return {
306
+ ok: false,
307
+ action: "refused",
308
+ detail: "Refusing to remove the legacy deft/ tree because it has local modifications or " +
309
+ "untracked files. Reconcile or commit those changes, then re-run " +
310
+ "`npx @deftai/directive update`, or remove deft/ manually once clean. " +
311
+ `Dirty paths: ${JSON.stringify(dirtyPaths.slice(0, 5))}` +
312
+ (dirtyPaths.length > 5 ? ", …" : "") +
313
+ ".",
314
+ };
315
+ }
316
+ const deftDir = join(projectDir, LEGACY_DEFT_DIR);
317
+ const removeDir = seams.removeDir ?? ((p) => rmSync(p, { recursive: true, force: true }));
318
+ const runGitRm = seams.runGitRm ??
319
+ ((root, relPath) => {
320
+ execFileSync("git", ["rm", "-r", "--ignore-unmatch", relPath], {
321
+ cwd: root,
322
+ encoding: "utf8",
323
+ stdio: ["ignore", "pipe", "pipe"],
324
+ });
325
+ });
326
+ try {
327
+ runGitRm(projectDir, LEGACY_DEFT_DIR);
328
+ removeDir(deftDir);
329
+ return {
330
+ ok: true,
331
+ action: "staged",
332
+ detail: "Removed the legacy deft/ framework tree and staged its deletion; " +
333
+ ".deft/core/ is now the sole framework root.",
334
+ };
335
+ }
336
+ catch {
337
+ try {
338
+ removeDir(deftDir);
339
+ return {
340
+ ok: true,
341
+ action: "removed",
342
+ detail: "Removed the legacy deft/ framework tree from disk; stage the deletion with " +
343
+ "`git add -u deft/` if it was tracked.",
344
+ };
345
+ }
346
+ catch (cause) {
347
+ return {
348
+ ok: false,
349
+ action: "refused",
350
+ detail: `Could not remove the legacy deft/ tree: ${String(cause)}`,
351
+ };
352
+ }
353
+ }
354
+ }
223
355
  //# sourceMappingURL=legacy-detect.js.map
@@ -47,6 +47,8 @@ export interface RefreshDepositSeams {
47
47
  gitLsFiles?: GitLsFiles;
48
48
  /** #2530: injected git config seams for {@link writeConsumerGitHooks}. */
49
49
  gitHooks?: GitHooksSeams;
50
+ /** #2822: optional seam for trusted-org policy force-on migration. */
51
+ runOrgForceOn?: (projectRoot: string) => void;
50
52
  }
51
53
  /**
52
54
  * The four states `directive update` classifies an EXISTING install into BEFORE
@@ -18,11 +18,13 @@ import { resolveInstalledContentRoot } from "../deposit/resolve-content.js";
18
18
  import { manifestTagToVersion, parseInstallManifest } from "../doctor/manifest.js";
19
19
  import { readCorePackageVersion } from "../engine-version.js";
20
20
  import { resolveLifecycleRoot } from "../layout/resolve.js";
21
+ import { runOrgForceOnMigration } from "../policy/org-force-on-migration.js";
21
22
  import { checkLocalEngineIntegrity, classify, ENGINE_PACKAGE, plan, renderGlobalInstall, resolveEngine, } from "../resolution/index.js";
22
23
  import { gitPorcelain } from "../story-ready/git.js";
24
+ import { removeStaleMigratedFrameworkNarrative } from "../xbrief-migrate/migrate-project.js";
23
25
  import { writeAgentHookDeposit } from "./agent-hooks.js";
24
26
  import { ensureInitGitignoreLines, isDepositTrackedInGit } from "./gitignore.js";
25
- import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, pruneStrayDepositPaths, } from "./hygiene.js";
27
+ import { depositStagePaths, isInstallerManagedPath, printCommitGuidance, prunePackageAbsentDepositPaths, } from "./hygiene.js";
26
28
  import { parseInitArgv } from "./init-deposit.js";
27
29
  import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
28
30
  import { printMigrateNudgeIfNeeded } from "./migrate.js";
@@ -409,10 +411,10 @@ export async function runRefreshDeposit(args, io, seams = {}) {
409
411
  else {
410
412
  await copyContent(contentRoot, deftDir);
411
413
  await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
412
- // #2347: prune framework-source paths that the content package does not ship.
413
- // The additive file-swap never removes them, causing the deposit-hygiene
414
- // advisory to persist across every upgrade until manually cleaned.
415
- await pruneStrayDepositPaths(deftDir, contentRoot, io);
414
+ // #2804 / #2347: prune deposit paths absent from the content package. The
415
+ // additive file-swap never removes them, causing stale bridge-era files to
416
+ // survive across upgrades until manually cleaned.
417
+ await prunePackageAbsentDepositPaths(deftDir, contentRoot, io);
416
418
  const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
417
419
  const manifestFields = {
418
420
  ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
@@ -441,6 +443,17 @@ export async function runRefreshDeposit(args, io, seams = {}) {
441
443
  // lifecycle content before migrate:xbrief can transactionally converge it.
442
444
  if (hasCanonicalXbriefLifecycle(projectDir)) {
443
445
  syncConsumerXbriefSchemas(projectDir, deftDir);
446
+ removeStaleMigratedFrameworkNarrative(projectDir);
447
+ }
448
+ const runOrgForceOn = seams.runOrgForceOn ??
449
+ ((root) => {
450
+ runOrgForceOnMigration(root, { actor: "directive-update" });
451
+ });
452
+ try {
453
+ runOrgForceOn(projectDir);
454
+ }
455
+ catch {
456
+ // Policy migration is best-effort; never block framework refresh (#2822).
444
457
  }
445
458
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
446
459
  writeAgentHookDeposit(projectDir, io);
@@ -4,6 +4,7 @@ export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
6
  export * from "./host-hooks.js";
7
+ export * from "./org-force-on-migration.js";
7
8
  export * from "./plan-extensions.js";
8
9
  export * from "./policy-invocation.js";
9
10
  export * from "./product-signal.js";
@@ -11,6 +12,7 @@ export * from "./resolve.js";
11
12
  export * from "./runtime-authority.js";
12
13
  export * from "./staleness-tickler.js";
13
14
  export * from "./value-feedback.js";
15
+ export * from "./value-feedback-autoenable.js";
14
16
  export * from "./wip.js";
15
17
  export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
16
18
  export declare const FIELD_WIP_CAP = "plan.policy.wipCap";
@@ -12,6 +12,7 @@ export * from "./capacity.js";
12
12
  export * from "./decisions.js";
13
13
  export * from "./disclosure.js";
14
14
  export * from "./host-hooks.js";
15
+ export * from "./org-force-on-migration.js";
15
16
  export * from "./plan-extensions.js";
16
17
  export * from "./policy-invocation.js";
17
18
  export * from "./product-signal.js";
@@ -19,6 +20,7 @@ export * from "./resolve.js";
19
20
  export * from "./runtime-authority.js";
20
21
  export * from "./staleness-tickler.js";
21
22
  export * from "./value-feedback.js";
23
+ export * from "./value-feedback-autoenable.js";
22
24
  export * from "./wip.js";
23
25
  export const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
24
26
  export const FIELD_WIP_CAP = "plan.policy.wipCap";
@@ -0,0 +1,44 @@
1
+ import { type OrgAutoEnableOptions } from "./value-feedback-autoenable.js";
2
+ /** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
3
+ export declare const ORG_FORCE_ON_MARKER_REL: string;
4
+ export declare const ORG_FORCE_ON_MARKER_VERSION: 1;
5
+ export interface OrgForceOnMarker {
6
+ readonly version: typeof ORG_FORCE_ON_MARKER_VERSION;
7
+ readonly appliedAt: string;
8
+ readonly originOrg: string;
9
+ readonly valueFeedback: boolean;
10
+ readonly productSignal: boolean;
11
+ readonly directiveVersion: string;
12
+ }
13
+ /** Canonical typed block written by the one-time migration (#2822 Part A). */
14
+ export declare const FORCE_ON_VALUE_FEEDBACK_BLOCK: {
15
+ readonly enabled: true;
16
+ readonly emitEvents: true;
17
+ readonly sessionLine: true;
18
+ readonly upstreamPrompt: false;
19
+ };
20
+ /** Read the #2822 migration marker when present and valid. */
21
+ export declare function readOrgForceOnMarker(projectRoot: string): OrgForceOnMarker | null;
22
+ export interface RunOrgForceOnMigrationOptions {
23
+ readonly autoEnable?: OrgAutoEnableOptions;
24
+ readonly actor?: string;
25
+ readonly now?: Date;
26
+ }
27
+ export interface OrgForceOnMigrationResult {
28
+ readonly ran: boolean;
29
+ readonly skippedReason: string | null;
30
+ readonly valueFeedbackChanged: boolean;
31
+ readonly productSignalChanged: boolean;
32
+ }
33
+ /**
34
+ * One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
35
+ * (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
36
+ */
37
+ export declare function runOrgForceOnMigration(projectRoot: string, options?: RunOrgForceOnMigrationOptions): OrgForceOnMigrationResult;
38
+ /** True when typed valueFeedback matches the #2822 force-on block shape. */
39
+ export declare function isForceOnValueFeedbackBlock(raw: unknown): boolean;
40
+ /** Resolve auditable source for valueFeedback after #2822 migration. */
41
+ export declare function valueFeedbackInstallForceOnSource(projectRoot: string, raw: unknown): "install-force-on" | null;
42
+ /** Resolve auditable source for productSignal after #2822 migration. */
43
+ export declare function productSignalInstallForceOnSource(projectRoot: string, raw: unknown): "install-force-on" | null;
44
+ //# sourceMappingURL=org-force-on-migration.d.ts.map
@@ -0,0 +1,242 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { readCorePackageVersion } from "../engine-version.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
5
+ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
6
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
7
+ import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
8
+ import { detectOriginOrg, isTrustedOrgAutoEnable, } from "./value-feedback-autoenable.js";
9
+ /** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
10
+ export const ORG_FORCE_ON_MARKER_REL = join(".deft-cache", "org-force-on-v2822.json");
11
+ export const ORG_FORCE_ON_MARKER_VERSION = 1;
12
+ /** Canonical typed block written by the one-time migration (#2822 Part A). */
13
+ export const FORCE_ON_VALUE_FEEDBACK_BLOCK = {
14
+ enabled: true,
15
+ emitEvents: true,
16
+ sessionLine: true,
17
+ upstreamPrompt: false,
18
+ };
19
+ const DEFAULT_PRODUCT_SIGNAL_SINK = "deftai/product-signal";
20
+ function markerPath(projectRoot) {
21
+ return resolve(projectRoot, ORG_FORCE_ON_MARKER_REL);
22
+ }
23
+ function parseMarker(raw) {
24
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
25
+ return null;
26
+ }
27
+ const rec = raw;
28
+ if (rec.version !== ORG_FORCE_ON_MARKER_VERSION ||
29
+ typeof rec.appliedAt !== "string" ||
30
+ typeof rec.originOrg !== "string" ||
31
+ typeof rec.valueFeedback !== "boolean" ||
32
+ typeof rec.productSignal !== "boolean" ||
33
+ typeof rec.directiveVersion !== "string") {
34
+ return null;
35
+ }
36
+ return {
37
+ version: ORG_FORCE_ON_MARKER_VERSION,
38
+ appliedAt: rec.appliedAt,
39
+ originOrg: rec.originOrg,
40
+ valueFeedback: rec.valueFeedback,
41
+ productSignal: rec.productSignal,
42
+ directiveVersion: rec.directiveVersion,
43
+ };
44
+ }
45
+ /** Read the #2822 migration marker when present and valid. */
46
+ export function readOrgForceOnMarker(projectRoot) {
47
+ const path = markerPath(projectRoot);
48
+ if (!existsSync(path)) {
49
+ return null;
50
+ }
51
+ try {
52
+ return parseMarker(JSON.parse(readFileSync(path, "utf8")));
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function writeOrgForceOnMarker(projectRoot, marker) {
59
+ const path = markerPath(projectRoot);
60
+ assertWriteTargetSafe(projectRoot, path);
61
+ mkdirSync(join(path, ".."), { recursive: true });
62
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
63
+ }
64
+ function readTypedBoolean(raw, key, fallback) {
65
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
66
+ return fallback;
67
+ }
68
+ const value = raw[key];
69
+ return typeof value === "boolean" ? value : fallback;
70
+ }
71
+ function valueFeedbackNeedsForceOn(raw) {
72
+ const enabled = readTypedBoolean(raw, "enabled", false);
73
+ if (!enabled) {
74
+ return true;
75
+ }
76
+ const emitEvents = readTypedBoolean(raw, "emitEvents", true);
77
+ const sessionLine = readTypedBoolean(raw, "sessionLine", true);
78
+ const upstreamPrompt = readTypedBoolean(raw, "upstreamPrompt", false);
79
+ return !emitEvents || !sessionLine || upstreamPrompt;
80
+ }
81
+ function productSignalNeedsForceOn(raw) {
82
+ return !readTypedBoolean(raw, "enabled", false);
83
+ }
84
+ function ensurePolicyBlock(plan) {
85
+ migrateLegacyPolicyKey(plan);
86
+ const existing = plan[PLAN_POLICY_KEY];
87
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
88
+ if (existing === undefined) {
89
+ plan[PLAN_POLICY_KEY] = {};
90
+ }
91
+ else {
92
+ throw new Error("plan.policy is not an object");
93
+ }
94
+ }
95
+ return plan[PLAN_POLICY_KEY];
96
+ }
97
+ function writeMarkerForSkip(projectRoot, options, valueFeedback, productSignal) {
98
+ const originOrg = detectOriginOrg(projectRoot, options.autoEnable) ?? "unknown";
99
+ const now = options.now ?? new Date();
100
+ writeOrgForceOnMarker(projectRoot, {
101
+ version: ORG_FORCE_ON_MARKER_VERSION,
102
+ appliedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
103
+ originOrg,
104
+ valueFeedback,
105
+ productSignal,
106
+ directiveVersion: readCorePackageVersion(),
107
+ });
108
+ }
109
+ /**
110
+ * One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
111
+ * (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
112
+ */
113
+ export function runOrgForceOnMigration(projectRoot, options = {}) {
114
+ if (readOrgForceOnMarker(projectRoot) !== null) {
115
+ return {
116
+ ran: false,
117
+ skippedReason: "marker-present",
118
+ valueFeedbackChanged: false,
119
+ productSignalChanged: false,
120
+ };
121
+ }
122
+ if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
123
+ return {
124
+ ran: false,
125
+ skippedReason: "non-trusted-org",
126
+ valueFeedbackChanged: false,
127
+ productSignalChanged: false,
128
+ };
129
+ }
130
+ const [data, err] = loadProjectDefinition(projectRoot);
131
+ if (data === null) {
132
+ return {
133
+ ran: false,
134
+ skippedReason: err ?? "project-definition-missing",
135
+ valueFeedbackChanged: false,
136
+ productSignalChanged: false,
137
+ };
138
+ }
139
+ const policyBlock = readPlanPolicy(data.plan);
140
+ const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
141
+ ? policyBlock
142
+ : {};
143
+ const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
144
+ const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
145
+ const needsVf = valueFeedbackNeedsForceOn(vfRaw);
146
+ const needsPs = productSignalNeedsForceOn(psRaw);
147
+ if (!needsVf && !needsPs) {
148
+ writeMarkerForSkip(projectRoot, options, false, false);
149
+ return {
150
+ ran: false,
151
+ skippedReason: "already-enabled",
152
+ valueFeedbackChanged: false,
153
+ productSignalChanged: false,
154
+ };
155
+ }
156
+ const path = projectDefinitionPath(projectRoot);
157
+ let valueFeedbackChanged = false;
158
+ let productSignalChanged = false;
159
+ projectDefinitionMutationLock(projectRoot, () => {
160
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
161
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
162
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
163
+ }
164
+ const rootData = parsed;
165
+ if (typeof rootData.plan !== "object" ||
166
+ rootData.plan === null ||
167
+ Array.isArray(rootData.plan)) {
168
+ if (rootData.plan === undefined) {
169
+ rootData.plan = {};
170
+ }
171
+ else {
172
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
173
+ }
174
+ }
175
+ const plan = rootData.plan;
176
+ const policy = ensurePolicyBlock(plan);
177
+ const previousVf = policy.valueFeedback;
178
+ const previousPs = policy.productSignal;
179
+ if (needsVf) {
180
+ policy.valueFeedback = { ...FORCE_ON_VALUE_FEEDBACK_BLOCK };
181
+ valueFeedbackChanged = valueFeedbackNeedsForceOn(previousVf);
182
+ }
183
+ if (needsPs) {
184
+ const prevObj = typeof previousPs === "object" && previousPs !== null && !Array.isArray(previousPs)
185
+ ? previousPs
186
+ : {};
187
+ const sinkRepo = typeof prevObj.sinkRepo === "string" && prevObj.sinkRepo.trim().length > 0
188
+ ? prevObj.sinkRepo.trim()
189
+ : DEFAULT_PRODUCT_SIGNAL_SINK;
190
+ policy.productSignal = { enabled: true, sinkRepo };
191
+ productSignalChanged = productSignalNeedsForceOn(previousPs);
192
+ }
193
+ if (valueFeedbackChanged || productSignalChanged) {
194
+ atomicWriteProjectDefinition(path, rootData);
195
+ }
196
+ const actor = options.actor ?? "directive-update";
197
+ appendAuditLog(projectRoot, [
198
+ `actor=${actor}`,
199
+ "org-force-on-v2822",
200
+ `valueFeedback=${needsVf ? "forced-on" : "unchanged"}`,
201
+ `productSignal=${needsPs ? "forced-on" : "unchanged"}`,
202
+ `previousValueFeedback=${JSON.stringify(previousVf ?? null)}`,
203
+ `previousProductSignal=${JSON.stringify(previousPs ?? null)}`,
204
+ ].join(" "));
205
+ writeMarkerForSkip(projectRoot, options, needsVf, needsPs);
206
+ return { changed: valueFeedbackChanged || productSignalChanged };
207
+ });
208
+ return {
209
+ ran: true,
210
+ skippedReason: null,
211
+ valueFeedbackChanged,
212
+ productSignalChanged,
213
+ };
214
+ }
215
+ /** True when typed valueFeedback matches the #2822 force-on block shape. */
216
+ export function isForceOnValueFeedbackBlock(raw) {
217
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
218
+ return false;
219
+ }
220
+ const block = raw;
221
+ return (block.enabled === FORCE_ON_VALUE_FEEDBACK_BLOCK.enabled &&
222
+ readTypedBoolean(raw, "emitEvents", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.emitEvents &&
223
+ readTypedBoolean(raw, "sessionLine", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.sessionLine &&
224
+ readTypedBoolean(raw, "upstreamPrompt", true) === FORCE_ON_VALUE_FEEDBACK_BLOCK.upstreamPrompt);
225
+ }
226
+ /** Resolve auditable source for valueFeedback after #2822 migration. */
227
+ export function valueFeedbackInstallForceOnSource(projectRoot, raw) {
228
+ const marker = readOrgForceOnMarker(projectRoot);
229
+ if (marker === null || !marker.valueFeedback) {
230
+ return null;
231
+ }
232
+ return isForceOnValueFeedbackBlock(raw) ? "install-force-on" : null;
233
+ }
234
+ /** Resolve auditable source for productSignal after #2822 migration. */
235
+ export function productSignalInstallForceOnSource(projectRoot, raw) {
236
+ const marker = readOrgForceOnMarker(projectRoot);
237
+ if (marker === null || !marker.productSignal) {
238
+ return null;
239
+ }
240
+ return readTypedBoolean(raw, "enabled", false) ? "install-force-on" : null;
241
+ }
242
+ //# sourceMappingURL=org-force-on-migration.js.map
@@ -9,7 +9,7 @@ export interface ProductSignalConfig {
9
9
  readonly enabled: boolean;
10
10
  readonly sinkRepo: string;
11
11
  }
12
- export type ProductSignalSource = "typed" | "default" | "default-on-error";
12
+ export type ProductSignalSource = "typed" | "install-force-on" | "default" | "default-on-error";
13
13
  export interface ProductSignalResolved extends ProductSignalConfig {
14
14
  readonly source: ProductSignalSource;
15
15
  readonly error: string | null;
@@ -17,6 +17,8 @@ export interface ProductSignalResolved extends ProductSignalConfig {
17
17
  export declare const PRODUCT_SIGNAL_CAPABILITY_COST_DISCLOSURE: string;
18
18
  /** Validate a `plan.policy.productSignal` payload. */
19
19
  export declare function validateProductSignal(value: unknown): string[];
20
+ /** Resolve a typed `productSignal` block without install-force-on overlay. */
21
+ export declare function resolveProductSignalFromTypedBlock(raw: unknown): ProductSignalResolved;
20
22
  /** Resolve `plan.policy.productSignal` from PROJECT-DEFINITION (#2693). */
21
23
  export declare function resolveProductSignal(projectRoot: string): ProductSignalResolved;
22
24
  /** Human-readable status line for CLI surfaces. */
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
3
+ import { productSignalInstallForceOnSource } from "./org-force-on-migration.js";
3
4
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
4
5
  import { policyColonInvocation } from "./policy-invocation.js";
5
6
  import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
@@ -48,7 +49,8 @@ export function validateProductSignal(value) {
48
49
  }
49
50
  return errors;
50
51
  }
51
- function resolveFromPolicyBlock(raw) {
52
+ /** Resolve a typed `productSignal` block without install-force-on overlay. */
53
+ export function resolveProductSignalFromTypedBlock(raw) {
52
54
  const errors = validateProductSignal(raw);
53
55
  if (errors.length > 0) {
54
56
  return defaultResolved("default-on-error", errors[0] ?? "invalid productSignal block");
@@ -81,7 +83,13 @@ export function resolveProductSignal(projectRoot) {
81
83
  !("productSignal" in policyBlock)) {
82
84
  return defaultResolved("default");
83
85
  }
84
- return resolveFromPolicyBlock(policyBlock.productSignal);
86
+ const raw = policyBlock.productSignal;
87
+ const installSource = productSignalInstallForceOnSource(projectRoot, raw);
88
+ if (installSource !== null) {
89
+ const resolved = resolveProductSignalFromTypedBlock(raw);
90
+ return { ...resolved, source: installSource };
91
+ }
92
+ return resolveProductSignalFromTypedBlock(raw);
85
93
  }
86
94
  /** Human-readable status line for CLI surfaces. */
87
95
  export function formatProductSignalStatusLine(policy) {
@@ -117,7 +125,10 @@ export function inspectProductSignal(data, projectRoot) {
117
125
  }
118
126
  return fieldFromResolved(defaultResolved("default"));
119
127
  }
120
- return fieldFromResolved(resolveFromPolicyBlock(policyBlock.productSignal));
128
+ const raw = policyBlock.productSignal;
129
+ const installSource = projectRoot !== undefined ? productSignalInstallForceOnSource(projectRoot, raw) : null;
130
+ const resolved = resolveProductSignalFromTypedBlock(raw);
131
+ return fieldFromResolved(installSource !== null ? { ...resolved, source: installSource } : resolved);
121
132
  }
122
133
  /** Persist `productSignal.enabled=true` after capability-cost disclosure (#2693). */
123
134
  export function enableProductSignal(projectRoot, options) {
@@ -168,7 +179,7 @@ export function enableProductSignal(projectRoot, options) {
168
179
  ? prevObj.sinkRepo.trim()
169
180
  : DEFAULT_PRODUCT_SIGNAL_SINK_REPO);
170
181
  const nextBlock = { enabled: true, sinkRepo };
171
- const previousNormalized = resolveFromPolicyBlock(previous);
182
+ const previousNormalized = resolveProductSignalFromTypedBlock(previous);
172
183
  const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
173
184
  previousNormalized.sinkRepo !== nextBlock.sinkRepo;
174
185
  policyBlock.productSignal = nextBlock;