@deftai/directive-core 0.84.0 → 0.85.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 (69) 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 +2 -0
  5. package/dist/doctor/constants.js +8 -4
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.js +13 -4
  9. package/dist/doctor/npm-registry.d.ts +23 -0
  10. package/dist/doctor/npm-registry.js +128 -0
  11. package/dist/doctor/npm-view.d.ts +10 -0
  12. package/dist/doctor/npm-view.js +31 -0
  13. package/dist/doctor/payload-staleness.js +2 -9
  14. package/dist/doctor/signpost-checks.d.ts +2 -1
  15. package/dist/doctor/signpost-checks.js +2 -0
  16. package/dist/doctor/types.d.ts +9 -0
  17. package/dist/eval-health-relocation/evaluate.js +15 -1
  18. package/dist/fs/projection-containment.js +1 -1
  19. package/dist/hooks/cursor-hooks.d.ts +6 -20
  20. package/dist/hooks/cursor-hooks.js +7 -92
  21. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  22. package/dist/init-deposit/agent-hooks.js +15 -37
  23. package/dist/init-deposit/hygiene.d.ts +22 -0
  24. package/dist/init-deposit/hygiene.js +136 -4
  25. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  26. package/dist/init-deposit/legacy-detect.js +143 -11
  27. package/dist/init-deposit/refresh.d.ts +2 -0
  28. package/dist/init-deposit/refresh.js +18 -5
  29. package/dist/policy/index.d.ts +2 -0
  30. package/dist/policy/index.js +2 -0
  31. package/dist/policy/org-force-on-migration.d.ts +44 -0
  32. package/dist/policy/org-force-on-migration.js +240 -0
  33. package/dist/policy/product-signal.d.ts +3 -1
  34. package/dist/policy/product-signal.js +15 -4
  35. package/dist/policy/value-feedback.d.ts +18 -1
  36. package/dist/policy/value-feedback.js +78 -5
  37. package/dist/preflight-cache/evaluate.js +12 -1
  38. package/dist/product-signal/consent-prompt.d.ts +13 -0
  39. package/dist/product-signal/consent-prompt.js +30 -0
  40. package/dist/product-signal/submit.js +22 -1
  41. package/dist/render/index.d.ts +1 -0
  42. package/dist/render/index.js +1 -0
  43. package/dist/render/rule-map-template.d.ts +2 -0
  44. package/dist/render/rule-map-template.js +407 -0
  45. package/dist/render/rule-map.d.ts +2 -0
  46. package/dist/render/rule-map.js +703 -0
  47. package/dist/review-monitor/constants.d.ts +7 -1
  48. package/dist/review-monitor/constants.js +40 -14
  49. package/dist/review-monitor/github-lease.d.ts +45 -0
  50. package/dist/review-monitor/github-lease.js +106 -0
  51. package/dist/review-monitor/index.d.ts +2 -0
  52. package/dist/review-monitor/index.js +2 -0
  53. package/dist/review-monitor/lease-comment.d.ts +36 -0
  54. package/dist/review-monitor/lease-comment.js +152 -0
  55. package/dist/review-monitor/record.d.ts +61 -13
  56. package/dist/review-monitor/record.js +379 -132
  57. package/dist/review-monitor/verify.d.ts +2 -0
  58. package/dist/review-monitor/verify.js +32 -13
  59. package/dist/scm/gh-rest.d.ts +7 -1
  60. package/dist/scm/gh-rest.js +35 -0
  61. package/dist/session/session-start.js +5 -0
  62. package/dist/task-surface/index.js +53 -13
  63. package/dist/xbrief-migrate/constants.d.ts +6 -0
  64. package/dist/xbrief-migrate/constants.js +6 -0
  65. package/dist/xbrief-migrate/index.d.ts +2 -2
  66. package/dist/xbrief-migrate/index.js +2 -2
  67. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  68. package/dist/xbrief-migrate/migrate-project.js +18 -1
  69. package/package.json +3 -3
@@ -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,240 @@
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 { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
5
+ import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
6
+ import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
7
+ import { detectOriginOrg, isTrustedOrgAutoEnable, } from "./value-feedback-autoenable.js";
8
+ /** Durable once-per-checkout marker for #2822 trusted-org install force-on. */
9
+ export const ORG_FORCE_ON_MARKER_REL = join(".deft-cache", "org-force-on-v2822.json");
10
+ export const ORG_FORCE_ON_MARKER_VERSION = 1;
11
+ /** Canonical typed block written by the one-time migration (#2822 Part A). */
12
+ export const FORCE_ON_VALUE_FEEDBACK_BLOCK = {
13
+ enabled: true,
14
+ emitEvents: true,
15
+ sessionLine: true,
16
+ upstreamPrompt: false,
17
+ };
18
+ const DEFAULT_PRODUCT_SIGNAL_SINK = "deftai/product-signal";
19
+ function markerPath(projectRoot) {
20
+ return resolve(projectRoot, ORG_FORCE_ON_MARKER_REL);
21
+ }
22
+ function parseMarker(raw) {
23
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
24
+ return null;
25
+ }
26
+ const rec = raw;
27
+ if (rec.version !== ORG_FORCE_ON_MARKER_VERSION ||
28
+ typeof rec.appliedAt !== "string" ||
29
+ typeof rec.originOrg !== "string" ||
30
+ typeof rec.valueFeedback !== "boolean" ||
31
+ typeof rec.productSignal !== "boolean" ||
32
+ typeof rec.directiveVersion !== "string") {
33
+ return null;
34
+ }
35
+ return {
36
+ version: ORG_FORCE_ON_MARKER_VERSION,
37
+ appliedAt: rec.appliedAt,
38
+ originOrg: rec.originOrg,
39
+ valueFeedback: rec.valueFeedback,
40
+ productSignal: rec.productSignal,
41
+ directiveVersion: rec.directiveVersion,
42
+ };
43
+ }
44
+ /** Read the #2822 migration marker when present and valid. */
45
+ export function readOrgForceOnMarker(projectRoot) {
46
+ const path = markerPath(projectRoot);
47
+ if (!existsSync(path)) {
48
+ return null;
49
+ }
50
+ try {
51
+ return parseMarker(JSON.parse(readFileSync(path, "utf8")));
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function writeOrgForceOnMarker(projectRoot, marker) {
58
+ const path = markerPath(projectRoot);
59
+ mkdirSync(join(path, ".."), { recursive: true });
60
+ writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
61
+ }
62
+ function readTypedBoolean(raw, key, fallback) {
63
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
64
+ return fallback;
65
+ }
66
+ const value = raw[key];
67
+ return typeof value === "boolean" ? value : fallback;
68
+ }
69
+ function valueFeedbackNeedsForceOn(raw) {
70
+ const enabled = readTypedBoolean(raw, "enabled", false);
71
+ if (!enabled) {
72
+ return true;
73
+ }
74
+ const emitEvents = readTypedBoolean(raw, "emitEvents", true);
75
+ const sessionLine = readTypedBoolean(raw, "sessionLine", true);
76
+ const upstreamPrompt = readTypedBoolean(raw, "upstreamPrompt", false);
77
+ return !emitEvents || !sessionLine || upstreamPrompt;
78
+ }
79
+ function productSignalNeedsForceOn(raw) {
80
+ return !readTypedBoolean(raw, "enabled", false);
81
+ }
82
+ function ensurePolicyBlock(plan) {
83
+ migrateLegacyPolicyKey(plan);
84
+ const existing = plan[PLAN_POLICY_KEY];
85
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
86
+ if (existing === undefined) {
87
+ plan[PLAN_POLICY_KEY] = {};
88
+ }
89
+ else {
90
+ throw new Error("plan.policy is not an object");
91
+ }
92
+ }
93
+ return plan[PLAN_POLICY_KEY];
94
+ }
95
+ function writeMarkerForSkip(projectRoot, options, valueFeedback, productSignal) {
96
+ const originOrg = detectOriginOrg(projectRoot, options.autoEnable) ?? "unknown";
97
+ const now = options.now ?? new Date();
98
+ writeOrgForceOnMarker(projectRoot, {
99
+ version: ORG_FORCE_ON_MARKER_VERSION,
100
+ appliedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
101
+ originOrg,
102
+ valueFeedback,
103
+ productSignal,
104
+ directiveVersion: readCorePackageVersion(),
105
+ });
106
+ }
107
+ /**
108
+ * One-time trusted-org install/upgrade force-on for valueFeedback + productSignal
109
+ * (#2822). Idempotent via durable marker; non-trusted orgs are unchanged.
110
+ */
111
+ export function runOrgForceOnMigration(projectRoot, options = {}) {
112
+ if (readOrgForceOnMarker(projectRoot) !== null) {
113
+ return {
114
+ ran: false,
115
+ skippedReason: "marker-present",
116
+ valueFeedbackChanged: false,
117
+ productSignalChanged: false,
118
+ };
119
+ }
120
+ if (!isTrustedOrgAutoEnable(projectRoot, options.autoEnable)) {
121
+ return {
122
+ ran: false,
123
+ skippedReason: "non-trusted-org",
124
+ valueFeedbackChanged: false,
125
+ productSignalChanged: false,
126
+ };
127
+ }
128
+ const [data, err] = loadProjectDefinition(projectRoot);
129
+ if (data === null) {
130
+ return {
131
+ ran: false,
132
+ skippedReason: err ?? "project-definition-missing",
133
+ valueFeedbackChanged: false,
134
+ productSignalChanged: false,
135
+ };
136
+ }
137
+ const policyBlock = readPlanPolicy(data.plan);
138
+ const policyObj = typeof policyBlock === "object" && policyBlock !== null && !Array.isArray(policyBlock)
139
+ ? policyBlock
140
+ : {};
141
+ const vfRaw = "valueFeedback" in policyObj ? policyObj.valueFeedback : undefined;
142
+ const psRaw = "productSignal" in policyObj ? policyObj.productSignal : undefined;
143
+ const needsVf = valueFeedbackNeedsForceOn(vfRaw);
144
+ const needsPs = productSignalNeedsForceOn(psRaw);
145
+ if (!needsVf && !needsPs) {
146
+ writeMarkerForSkip(projectRoot, options, false, false);
147
+ return {
148
+ ran: false,
149
+ skippedReason: "already-enabled",
150
+ valueFeedbackChanged: false,
151
+ productSignalChanged: false,
152
+ };
153
+ }
154
+ const path = projectDefinitionPath(projectRoot);
155
+ let valueFeedbackChanged = false;
156
+ let productSignalChanged = false;
157
+ projectDefinitionMutationLock(projectRoot, () => {
158
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
159
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
160
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
161
+ }
162
+ const rootData = parsed;
163
+ if (typeof rootData.plan !== "object" ||
164
+ rootData.plan === null ||
165
+ Array.isArray(rootData.plan)) {
166
+ if (rootData.plan === undefined) {
167
+ rootData.plan = {};
168
+ }
169
+ else {
170
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
171
+ }
172
+ }
173
+ const plan = rootData.plan;
174
+ const policy = ensurePolicyBlock(plan);
175
+ const previousVf = policy.valueFeedback;
176
+ const previousPs = policy.productSignal;
177
+ if (needsVf) {
178
+ policy.valueFeedback = { ...FORCE_ON_VALUE_FEEDBACK_BLOCK };
179
+ valueFeedbackChanged = valueFeedbackNeedsForceOn(previousVf);
180
+ }
181
+ if (needsPs) {
182
+ const prevObj = typeof previousPs === "object" && previousPs !== null && !Array.isArray(previousPs)
183
+ ? previousPs
184
+ : {};
185
+ const sinkRepo = typeof prevObj.sinkRepo === "string" && prevObj.sinkRepo.trim().length > 0
186
+ ? prevObj.sinkRepo.trim()
187
+ : DEFAULT_PRODUCT_SIGNAL_SINK;
188
+ policy.productSignal = { enabled: true, sinkRepo };
189
+ productSignalChanged = productSignalNeedsForceOn(previousPs);
190
+ }
191
+ if (valueFeedbackChanged || productSignalChanged) {
192
+ atomicWriteProjectDefinition(path, rootData);
193
+ }
194
+ const actor = options.actor ?? "directive-update";
195
+ appendAuditLog(projectRoot, [
196
+ `actor=${actor}`,
197
+ "org-force-on-v2822",
198
+ `valueFeedback=${needsVf ? "forced-on" : "unchanged"}`,
199
+ `productSignal=${needsPs ? "forced-on" : "unchanged"}`,
200
+ `previousValueFeedback=${JSON.stringify(previousVf ?? null)}`,
201
+ `previousProductSignal=${JSON.stringify(previousPs ?? null)}`,
202
+ ].join(" "));
203
+ writeMarkerForSkip(projectRoot, options, needsVf, needsPs);
204
+ return { changed: valueFeedbackChanged || productSignalChanged };
205
+ });
206
+ return {
207
+ ran: true,
208
+ skippedReason: null,
209
+ valueFeedbackChanged,
210
+ productSignalChanged,
211
+ };
212
+ }
213
+ /** True when typed valueFeedback matches the #2822 force-on block shape. */
214
+ export function isForceOnValueFeedbackBlock(raw) {
215
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
216
+ return false;
217
+ }
218
+ const block = raw;
219
+ return (block.enabled === FORCE_ON_VALUE_FEEDBACK_BLOCK.enabled &&
220
+ readTypedBoolean(raw, "emitEvents", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.emitEvents &&
221
+ readTypedBoolean(raw, "sessionLine", false) === FORCE_ON_VALUE_FEEDBACK_BLOCK.sessionLine &&
222
+ readTypedBoolean(raw, "upstreamPrompt", true) === FORCE_ON_VALUE_FEEDBACK_BLOCK.upstreamPrompt);
223
+ }
224
+ /** Resolve auditable source for valueFeedback after #2822 migration. */
225
+ export function valueFeedbackInstallForceOnSource(projectRoot, raw) {
226
+ const marker = readOrgForceOnMarker(projectRoot);
227
+ if (marker === null || !marker.valueFeedback) {
228
+ return null;
229
+ }
230
+ return isForceOnValueFeedbackBlock(raw) ? "install-force-on" : null;
231
+ }
232
+ /** Resolve auditable source for productSignal after #2822 migration. */
233
+ export function productSignalInstallForceOnSource(projectRoot, raw) {
234
+ const marker = readOrgForceOnMarker(projectRoot);
235
+ if (marker === null || !marker.productSignal) {
236
+ return null;
237
+ }
238
+ return readTypedBoolean(raw, "enabled", false) ? "install-force-on" : null;
239
+ }
240
+ //# 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;
@@ -17,7 +17,7 @@ export interface ValueFeedbackConfig {
17
17
  readonly sessionLine: boolean;
18
18
  readonly upstreamPrompt: boolean;
19
19
  }
20
- export type ValueFeedbackSource = "typed" | "org-auto" | "default" | "default-on-error";
20
+ export type ValueFeedbackSource = "typed" | "org-auto" | "install-force-on" | "default" | "default-on-error";
21
21
  export interface ValueFeedbackResolved extends ValueFeedbackConfig {
22
22
  readonly source: ValueFeedbackSource;
23
23
  readonly error: string | null;
@@ -25,6 +25,8 @@ export interface ValueFeedbackResolved extends ValueFeedbackConfig {
25
25
  export declare const VALUE_FEEDBACK_CAPABILITY_COST_DISCLOSURE: string;
26
26
  /** Validate a `plan.policy.valueFeedback` payload. */
27
27
  export declare function validateValueFeedback(value: unknown): string[];
28
+ /** Resolve a typed `valueFeedback` block without org-auto / install-force-on layers. */
29
+ export declare function resolveValueFeedbackFromTypedBlock(raw: unknown): ValueFeedbackResolved;
28
30
  export interface ResolveValueFeedbackOptions {
29
31
  /** Test seam for origin-org auto-enable resolution (#2376). */
30
32
  readonly autoEnable?: OrgAutoEnableOptions;
@@ -78,4 +80,19 @@ export interface EnableValueFeedbackResult {
78
80
  }
79
81
  /** Persist `valueFeedback.enabled=true` after capability-cost disclosure (#1709). */
80
82
  export declare function enableValueFeedback(projectRoot: string, options: EnableValueFeedbackOptions): EnableValueFeedbackResult;
83
+ export interface ClearValueFeedbackOptions {
84
+ readonly actor?: string;
85
+ readonly note?: string;
86
+ }
87
+ export interface ClearValueFeedbackResult {
88
+ readonly exitCode: 0 | 2;
89
+ readonly stdout: string;
90
+ readonly changed: boolean;
91
+ }
92
+ /**
93
+ * Remove the typed `valueFeedback` key so trusted-org resolution can return to
94
+ * org-auto (#2822). Does not delete the install-force-on marker — subsequent
95
+ * updates will not re-force against intentional opt-out.
96
+ */
97
+ export declare function clearValueFeedback(projectRoot: string, options?: ClearValueFeedbackOptions): ClearValueFeedbackResult;
81
98
  //# sourceMappingURL=value-feedback.d.ts.map
@@ -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 { valueFeedbackInstallForceOnSource } 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";
@@ -77,7 +78,8 @@ export function validateValueFeedback(value) {
77
78
  }
78
79
  return errors;
79
80
  }
80
- function resolveFromPolicyBlock(raw) {
81
+ /** Resolve a typed `valueFeedback` block without org-auto / install-force-on layers. */
82
+ export function resolveValueFeedbackFromTypedBlock(raw) {
81
83
  const errors = validateValueFeedback(raw);
82
84
  if (errors.length > 0) {
83
85
  return defaultResolved("default-on-error", errors[0] ?? "invalid valueFeedback block");
@@ -130,7 +132,13 @@ export function resolveValueFeedback(projectRoot, options = {}) {
130
132
  }
131
133
  return defaultResolved("default");
132
134
  }
133
- return resolveFromPolicyBlock(policyBlock.valueFeedback);
135
+ const raw = policyBlock.valueFeedback;
136
+ const installSource = valueFeedbackInstallForceOnSource(projectRoot, raw);
137
+ if (installSource !== null) {
138
+ const resolved = resolveValueFeedbackFromTypedBlock(raw);
139
+ return { ...resolved, source: installSource };
140
+ }
141
+ return resolveValueFeedbackFromTypedBlock(raw);
134
142
  }
135
143
  /** Master gate: when `enabled` is false, every downstream path is rejected. */
136
144
  export function isValueFeedbackPathAllowed(path, policy) {
@@ -196,8 +204,10 @@ export function inspectValueFeedback(data, projectRoot, options = {}) {
196
204
  }
197
205
  return fieldFromResolved(defaultResolved("default"));
198
206
  }
199
- const resolved = resolveFromPolicyBlock(policyBlock.valueFeedback);
200
- return fieldFromResolved(resolved);
207
+ const raw = policyBlock.valueFeedback;
208
+ const installSource = projectRoot !== undefined ? valueFeedbackInstallForceOnSource(projectRoot, raw) : null;
209
+ const resolved = resolveValueFeedbackFromTypedBlock(raw);
210
+ return fieldFromResolved(installSource !== null ? { ...resolved, source: installSource } : resolved);
201
211
  }
202
212
  /** Persist `valueFeedback.enabled=true` after capability-cost disclosure (#1709). */
203
213
  export function enableValueFeedback(projectRoot, options) {
@@ -259,7 +269,7 @@ export function enableValueFeedback(projectRoot, options) {
259
269
  sessionLine: readPersistedSubFlag("sessionLine"),
260
270
  upstreamPrompt: readPersistedSubFlag("upstreamPrompt"),
261
271
  };
262
- const previousNormalized = resolveFromPolicyBlock(previous);
272
+ const previousNormalized = resolveValueFeedbackFromTypedBlock(previous);
263
273
  const changedFlag = previousNormalized.enabled !== nextBlock.enabled ||
264
274
  previousNormalized.emitEvents !== nextBlock.emitEvents ||
265
275
  previousNormalized.sessionLine !== nextBlock.sessionLine ||
@@ -302,4 +312,67 @@ export function enableValueFeedback(projectRoot, options) {
302
312
  return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
303
313
  }
304
314
  }
315
+ /**
316
+ * Remove the typed `valueFeedback` key so trusted-org resolution can return to
317
+ * org-auto (#2822). Does not delete the install-force-on marker — subsequent
318
+ * updates will not re-force against intentional opt-out.
319
+ */
320
+ export function clearValueFeedback(projectRoot, options = {}) {
321
+ const path = projectDefinitionPath(projectRoot);
322
+ try {
323
+ const { changed } = projectDefinitionMutationLock(projectRoot, () => {
324
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
325
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
326
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
327
+ }
328
+ const data = parsed;
329
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
330
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
331
+ }
332
+ const plan = data.plan;
333
+ migrateLegacyPolicyKey(plan);
334
+ const existingPolicy = plan[PLAN_POLICY_KEY];
335
+ if (typeof existingPolicy !== "object" ||
336
+ existingPolicy === null ||
337
+ Array.isArray(existingPolicy)) {
338
+ return { changed: false };
339
+ }
340
+ const policyBlock = existingPolicy;
341
+ if (!("valueFeedback" in policyBlock)) {
342
+ return { changed: false };
343
+ }
344
+ const previous = policyBlock.valueFeedback;
345
+ delete policyBlock.valueFeedback;
346
+ atomicWriteProjectDefinition(path, data);
347
+ const actor = options.actor ?? policyColonInvocation("clear-value-feedback");
348
+ const note = options.note ?? "";
349
+ const parts = [
350
+ `actor=${actor}`,
351
+ "valueFeedback=cleared",
352
+ `previous=${JSON.stringify(previous ?? null)}`,
353
+ ];
354
+ if (note) {
355
+ parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
356
+ }
357
+ appendAuditLog(projectRoot, parts.join(" "));
358
+ return { changed: true };
359
+ });
360
+ const resolved = resolveValueFeedback(projectRoot);
361
+ const lines = [
362
+ `\u2713 ${FIELD_VALUE_FEEDBACK} typed key removed (resolution returns to org-auto/default).`,
363
+ changed
364
+ ? " audit: meta/policy-changes.log updated."
365
+ : " no-op: typed key was already absent.",
366
+ formatValueFeedbackStatusLine(resolved),
367
+ ];
368
+ return { exitCode: 0, stdout: `${lines.join("\n")}\n`, changed };
369
+ }
370
+ catch (err) {
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ if (message.includes("PROJECT-DEFINITION not found")) {
373
+ return { exitCode: 2, stdout: `\u274c ${message}\n`, changed: false };
374
+ }
375
+ return { exitCode: 2, stdout: `\u274c Config error: ${message}\n`, changed: false };
376
+ }
377
+ }
305
378
  //# sourceMappingURL=value-feedback.js.map
@@ -14,7 +14,7 @@ import { execFileSync } from "node:child_process";
14
14
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
15
15
  import { join, relative } from "node:path";
16
16
  import { isTriageCacheEmpty, maybeAutoPopulateEmptyCache } from "../cache/empty-populate.js";
17
- import { probeCacheDrift } from "../cache/fetch.js";
17
+ import { probeCacheDrift, readOpenInventoryStamp, } from "../cache/fetch.js";
18
18
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
19
19
  import { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
20
20
  export { recoveryHintForStaleFailure } from "../session/cache-recovery.js";
@@ -423,6 +423,17 @@ function evaluateWithContext(projectRoot, options, ctx) {
423
423
  minMetaPath = p;
424
424
  }
425
425
  }
426
+ // #2826: empty open inventory with a successful fetch stamp is fresh — not
427
+ // the never-fetched case that yields minFetchedAt=null / Infinity age.
428
+ if (minFetchedAt === null && rawCandidatePaths.length === 0 && resolvedRepo !== null) {
429
+ const stamp = readOpenInventoryStamp(cacheRoot, source, resolvedRepo);
430
+ if (stamp !== null) {
431
+ const parsed = new Date(stamp.fetched_at);
432
+ if (!Number.isNaN(parsed.getTime())) {
433
+ minFetchedAt = parsed;
434
+ }
435
+ }
436
+ }
426
437
  const now = nowFn();
427
438
  const ageMs = minFetchedAt !== null ? now.getTime() - minFetchedAt.getTime() : Infinity;
428
439
  const ageH = ageMs / (1000 * 3600);
@@ -0,0 +1,13 @@
1
+ import { type HeadlessDetectionOptions } from "./headless.js";
2
+ /** D17 first-time consent ask (#2693 / #2822). */
3
+ export declare const PRODUCT_SIGNAL_CONSENT_PROMPT: string;
4
+ export interface ProductSignalConsentEligibilityOptions extends HeadlessDetectionOptions {
5
+ readonly projectRoot: string;
6
+ }
7
+ /** True when an interactive session should surface the D17 consent ask (#2822 AC). */
8
+ export declare function isProductSignalConsentEligible(options: ProductSignalConsentEligibilityOptions): boolean;
9
+ /** Format the D17 consent ask for session start / status surfaces. */
10
+ export declare function formatProductSignalConsentPrompt(): string;
11
+ /** Emit the D17 ask when eligible; headless callers get an empty string (#2693 D16). */
12
+ export declare function maybeFormatProductSignalConsentPrompt(options: ProductSignalConsentEligibilityOptions): string;
13
+ //# sourceMappingURL=consent-prompt.d.ts.map