@deftai/directive-core 0.73.0 → 0.74.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 (58) hide show
  1. package/dist/branch/evaluate.js +2 -1
  2. package/dist/check/orchestrator.d.ts +3 -0
  3. package/dist/check/orchestrator.js +2 -1
  4. package/dist/codebase/map.js +2 -0
  5. package/dist/deposit/copy-tree.js +31 -7
  6. package/dist/doctor/checks.d.ts +9 -0
  7. package/dist/doctor/checks.js +67 -0
  8. package/dist/doctor/main.js +2 -1
  9. package/dist/fs/projection-containment.d.ts +35 -0
  10. package/dist/fs/projection-containment.js +118 -0
  11. package/dist/init-deposit/hygiene.d.ts +6 -0
  12. package/dist/init-deposit/hygiene.js +5 -1
  13. package/dist/init-deposit/refresh.d.ts +16 -2
  14. package/dist/init-deposit/refresh.js +144 -50
  15. package/dist/init-deposit/scaffold.d.ts +21 -0
  16. package/dist/init-deposit/scaffold.js +84 -10
  17. package/dist/policy/disclosure.d.ts +1 -0
  18. package/dist/policy/disclosure.js +1 -0
  19. package/dist/policy/index.d.ts +1 -0
  20. package/dist/policy/index.js +1 -0
  21. package/dist/policy/policy-invocation.d.ts +5 -0
  22. package/dist/policy/policy-invocation.js +9 -0
  23. package/dist/policy/resolve.js +4 -2
  24. package/dist/policy/value-feedback.js +6 -3
  25. package/dist/pr-merge-readiness/constants.d.ts +4 -0
  26. package/dist/pr-merge-readiness/constants.js +4 -0
  27. package/dist/pr-merge-readiness/evaluate.js +3 -0
  28. package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
  29. package/dist/pr-merge-readiness/mergeability.js +15 -10
  30. package/dist/pr-merge-readiness/output.js +12 -6
  31. package/dist/pr-merge-readiness/parse.d.ts +1 -0
  32. package/dist/pr-merge-readiness/parse.js +9 -1
  33. package/dist/pr-merge-readiness/types.d.ts +2 -0
  34. package/dist/preflight-cache/evaluate.d.ts +2 -0
  35. package/dist/preflight-cache/evaluate.js +14 -3
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/release/constants.d.ts +2 -0
  39. package/dist/release/constants.js +2 -0
  40. package/dist/release/preflight.d.ts +2 -0
  41. package/dist/release/preflight.js +14 -1
  42. package/dist/scope/index.d.ts +1 -0
  43. package/dist/scope/index.js +1 -0
  44. package/dist/scope/main.js +24 -1
  45. package/dist/scope/open-umbrella-warning.d.ts +12 -0
  46. package/dist/scope/open-umbrella-warning.js +403 -0
  47. package/dist/swarm/constants.d.ts +1 -1
  48. package/dist/swarm/constants.js +2 -1
  49. package/dist/swarm/subagent-backend.js +2 -1
  50. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  51. package/dist/triage/bootstrap/gitignore.js +69 -58
  52. package/dist/ts-check-lane/run-lane.d.ts +4 -0
  53. package/dist/ts-check-lane/run-lane.js +20 -6
  54. package/dist/value/feedback-file.js +3 -2
  55. package/dist/value/readback.js +3 -2
  56. package/dist/vbrief-reconcile/umbrellas.js +12 -11
  57. package/dist/xbrief-migrate/migrate-project.js +9 -0
  58. package/package.json +3 -3
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * Refs #1942, #1430, #1671.
9
9
  */
10
+ import { execFileSync } from "node:child_process";
10
11
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
11
12
  import { platform as osPlatform } from "node:os";
12
13
  import { join, resolve } from "node:path";
@@ -218,11 +219,12 @@ function unquoteGitPath(path) {
218
219
  }
219
220
  return path;
220
221
  }
221
- function porcelainStatusPaths(porcelain) {
222
- const paths = [];
222
+ function porcelainStatusEntries(porcelain) {
223
+ const entries = [];
223
224
  for (const line of porcelain.split("\n")) {
224
225
  if (line.length < 4)
225
226
  continue;
227
+ const status = line.slice(0, 2);
226
228
  let rest = line.slice(3);
227
229
  const arrow = rest.indexOf(" -> ");
228
230
  if (arrow >= 0)
@@ -230,43 +232,102 @@ function porcelainStatusPaths(porcelain) {
230
232
  const trimmed = unquoteGitPath(rest.trim());
231
233
  if (!trimmed)
232
234
  continue;
233
- paths.push(trimmed.replace(/\\/g, "/"));
235
+ entries.push({ status, path: trimmed.replace(/\\/g, "/") });
234
236
  }
235
- return paths;
237
+ return entries;
236
238
  }
237
- function classifyChangedPaths(changed) {
239
+ function classifyChangedEntries(changed) {
238
240
  const core = [];
239
241
  const installerManaged = [];
240
- for (const path of changed) {
242
+ for (const entry of changed) {
243
+ const { path } = entry;
241
244
  if (!path)
242
245
  continue;
243
246
  if (path.startsWith(".deft/core/") || path === ".deft/core") {
244
- core.push(path);
247
+ core.push(entry);
245
248
  }
246
249
  else if (isInstallerManagedPath(path)) {
247
- installerManaged.push(path);
250
+ installerManaged.push(entry);
248
251
  }
249
252
  }
250
253
  return { core, installerManaged };
251
254
  }
255
+ function isCrlfNoiseCandidate(status) {
256
+ return status.includes("M") && /^[ M]+$/.test(status);
257
+ }
258
+ function normalizeGitNameList(output) {
259
+ return output
260
+ .split("\n")
261
+ .map((line) => line.trim().replace(/\\/g, "/"))
262
+ .filter(Boolean);
263
+ }
264
+ function gitSemanticDiffNames(projectRoot, paths) {
265
+ if (paths.length === 0)
266
+ return [];
267
+ try {
268
+ const args = ["diff", "--ignore-cr-at-eol", "--name-only", "--", ...paths];
269
+ const worktree = execFileSync("git", args, {
270
+ cwd: projectRoot,
271
+ encoding: "utf8",
272
+ stdio: ["ignore", "pipe", "pipe"],
273
+ });
274
+ const cached = execFileSync("git", ["diff", "--cached", "--ignore-cr-at-eol", "--name-only", "--", ...paths], {
275
+ cwd: projectRoot,
276
+ encoding: "utf8",
277
+ stdio: ["ignore", "pipe", "pipe"],
278
+ });
279
+ return [...new Set([...normalizeGitNameList(worktree), ...normalizeGitNameList(cached)])];
280
+ }
281
+ catch {
282
+ return null;
283
+ }
284
+ }
252
285
  /** Framework-managed uncommitted paths after refresh (#1671). */
253
- export function frameworkRefreshSideEffects(projectDir, readPorcelain = gitPorcelain) {
286
+ export function frameworkRefreshSideEffects(projectDir, options) {
287
+ const readPorcelain = options?.readPorcelain ?? gitPorcelain;
288
+ const readSemanticDiffNames = options?.readSemanticDiffNames ?? gitSemanticDiffNames;
254
289
  const porcelain = readPorcelain(projectDir);
255
290
  if (porcelain === null)
256
- return [];
257
- const changed = porcelainStatusPaths(porcelain);
258
- const { core, installerManaged } = classifyChangedPaths(changed);
259
- const files = [...core, ...installerManaged].sort();
260
- return files.length > 0 ? files : [];
291
+ return { files: [], crlfOnlyCoreFiles: [] };
292
+ const changed = porcelainStatusEntries(porcelain);
293
+ const { core, installerManaged } = classifyChangedEntries(changed);
294
+ const semanticCoreNames = core.length > 0
295
+ ? readSemanticDiffNames(projectDir, core.map((entry) => entry.path))
296
+ : [];
297
+ const semanticCoreSet = semanticCoreNames === null ? null : new Set(semanticCoreNames);
298
+ const coreFiles = [];
299
+ const crlfOnlyCoreFiles = [];
300
+ for (const entry of core) {
301
+ if (semanticCoreSet !== null &&
302
+ isCrlfNoiseCandidate(entry.status) &&
303
+ !semanticCoreSet.has(entry.path)) {
304
+ crlfOnlyCoreFiles.push(entry.path);
305
+ continue;
306
+ }
307
+ coreFiles.push(entry.path);
308
+ }
309
+ const files = [...coreFiles, ...installerManaged.map((entry) => entry.path)].sort();
310
+ return { files, crlfOnlyCoreFiles: crlfOnlyCoreFiles.sort() };
261
311
  }
262
- export function printRefreshSideEffects(io, files) {
263
- if (files.length === 0)
312
+ export function printRefreshSideEffects(io, effects) {
313
+ if (effects.crlfOnlyCoreFiles.length > 0) {
314
+ io.printf("\nWindows line-ending note (#2118): suppressed .deft/core CRLF/LF-only noise; " +
315
+ "ensure .gitattributes contains `.deft/core/** text eol=lf`.\n");
316
+ }
317
+ if (effects.files.length === 0)
318
+ return;
319
+ if (effects.payloadSwapped === false) {
320
+ io.printf("\nFramework-managed files still have semantic uncommitted changes:\n");
321
+ for (const file of effects.files) {
322
+ io.printf(` ${file}\n`);
323
+ }
264
324
  return;
325
+ }
265
326
  io.printf("\nAGENTS.md refresh side effects (#1671): the refresh and framework payload swap\n");
266
327
  io.printf("left these framework files with uncommitted changes -- they belong in the\n");
267
328
  io.printf("framework deposit commit (the installer stages them before printing the\n");
268
329
  io.printf("`git add` list below, so there are no post-stage stragglers):\n");
269
- for (const file of files) {
330
+ for (const file of effects.files) {
270
331
  io.printf(` ${file}\n`);
271
332
  }
272
333
  }
@@ -307,7 +368,8 @@ export function buildUpdateSummaryJson(result, options, updateState) {
307
368
  user_config_dir: "",
308
369
  skills_created: false,
309
370
  payload_layout: "vendored",
310
- strategy: "file-swap",
371
+ strategy: result.strategy,
372
+ already_current: result.alreadyCurrent,
311
373
  dirty_tree: false,
312
374
  dirty_files: [],
313
375
  staged_paths: result.stagedPaths,
@@ -319,9 +381,12 @@ export function buildUpdateSummaryJson(result, options, updateState) {
319
381
  };
320
382
  }
321
383
  export function printUpdateComplete(result, io, updateState) {
322
- io.printf("\n✓ Deft framework payload refreshed.\n\n");
384
+ io.printf(result.alreadyCurrent
385
+ ? "\nOK Deft framework payload already current.\n\n"
386
+ : "\nOK Deft framework payload refreshed.\n\n");
323
387
  io.printf(` Location : ${result.deftDir}\n`);
324
388
  io.printf(` Content : v${normalizeVersion(result.contentVersion)}\n`);
389
+ io.printf(` Strategy : ${result.strategy}\n`);
325
390
  if (updateState) {
326
391
  io.printf(` State : ${updateState}\n`);
327
392
  }
@@ -356,31 +421,41 @@ export async function runRefreshDeposit(args, io, seams = {}) {
356
421
  const engineVersion = readEngine();
357
422
  const contentVersion = readContentPackageVersion(contentRoot, readPackageVersion);
358
423
  const versionSkewNotice = buildVersionSkewNotice(engineVersion, contentVersion, previousDepositVersion);
359
- await copyContent(contentRoot, deftDir);
360
- await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
361
- // #2347: prune framework-source paths that the content package does not ship.
362
- // The additive file-swap never removes them, causing the deposit-hygiene
363
- // advisory to persist across every upgrade until manually cleaned.
364
- await pruneStrayDepositPaths(deftDir, contentRoot, io);
365
- const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
366
- const manifestFields = {
367
- ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
368
- sha: "content-package",
369
- tag: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
370
- installRoot: CANONICAL_INSTALL_ROOT,
371
- fetchedAt: nowIso(),
372
- fetchedBy: "directive-update",
373
- ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
374
- };
375
- const writtenManifestPath = writeInstallManifest(projectDir, deftDir, manifestFields);
376
- // #2064: retire a stale legacy .deft/VERSION now that the canonical
377
- // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
378
- // manifest behavior is lost by the redirect). Best-effort; never fatal.
379
- migrateLegacyInstallManifest(projectDir, writtenManifestPath);
380
- // #2055: regenerate the bare .deft-version derivative so it agrees with the
381
- // freshly written manifest tag (otherwise doctor's manifest-agreement check
382
- // fails and the operator must hand-edit the marker).
383
- syncBareVersionMarker(projectDir, contentVersion);
424
+ const alreadyCurrent = previousDepositVersion !== null &&
425
+ normalizeVersion(previousDepositVersion) === normalizeVersion(contentVersion);
426
+ const strategy = alreadyCurrent ? "no-op" : "file-swap";
427
+ if (alreadyCurrent) {
428
+ io.printf("[deft update] Framework payload already current; skipping payload copy.\n");
429
+ migrateLegacyInstallManifest(projectDir, join(deftDir, "VERSION"));
430
+ }
431
+ else {
432
+ await copyContent(contentRoot, deftDir);
433
+ await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
434
+ // #2347: prune framework-source paths that the content package does not ship.
435
+ // The additive file-swap never removes them, causing the deposit-hygiene
436
+ // advisory to persist across every upgrade until manually cleaned.
437
+ await pruneStrayDepositPaths(deftDir, contentRoot, io);
438
+ const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
439
+ const manifestFields = {
440
+ ref: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
441
+ sha: "content-package",
442
+ tag: contentVersion.startsWith("v") ? contentVersion : `v${contentVersion}`,
443
+ installRoot: CANONICAL_INSTALL_ROOT,
444
+ fetchedAt: nowIso(),
445
+ fetchedBy: "directive-update",
446
+ ...(previousManagedBy ? { managedBy: previousManagedBy } : {}),
447
+ };
448
+ const writtenManifestPath = writeInstallManifest(projectDir, deftDir, manifestFields);
449
+ // #2064: retire a stale legacy .deft/VERSION now that the canonical
450
+ // .deft/core/VERSION has been rewritten (folded in from install-upgrade so no
451
+ // manifest behavior is lost by the redirect). Best-effort; never fatal.
452
+ migrateLegacyInstallManifest(projectDir, writtenManifestPath);
453
+ // #2055: regenerate the bare .deft-version derivative so it agrees with the
454
+ // freshly written manifest tag (otherwise doctor's manifest-agreement check
455
+ // fails and the operator must hand-edit the marker). No-op refreshes skip
456
+ // this projection because the manifest tag was not rewritten (#2118).
457
+ syncBareVersionMarker(projectDir, contentVersion);
458
+ }
384
459
  const agentsMdUpdated = writeAgentsMd(projectDir, deftDir, io);
385
460
  // #2148: the deft-core-guard CI workflow is only meaningful when the deposit
386
461
  // is git-tracked (committed vendor layout). On an npm-managed (gitignored)
@@ -399,12 +474,27 @@ export async function runRefreshDeposit(args, io, seams = {}) {
399
474
  if (args.nonInteractive) {
400
475
  taskfileWired = ensureTaskfile(projectDir, io);
401
476
  }
402
- const { stagePaths, staged, stagedPaths } = depositStagePaths(projectDir, {
403
- includeTaskfile: taskfileWired,
404
- });
405
- printCommitGuidance(io, stagePaths, staged);
406
477
  const readPorcelain = seams.gitPorcelain ?? gitPorcelain;
407
- printRefreshSideEffects(io, frameworkRefreshSideEffects(projectDir, readPorcelain));
478
+ const effects = frameworkRefreshSideEffects(projectDir, {
479
+ readPorcelain,
480
+ readSemanticDiffNames: seams.gitSemanticDiffNames ?? gitSemanticDiffNames,
481
+ });
482
+ let stagedPaths = [];
483
+ if (!alreadyCurrent || effects.files.length > 0) {
484
+ const stagedResult = depositStagePaths(projectDir, {
485
+ includeTaskfile: taskfileWired,
486
+ includeCore: !alreadyCurrent,
487
+ });
488
+ stagedPaths = stagedResult.stagedPaths;
489
+ if (!alreadyCurrent) {
490
+ printCommitGuidance(io, stagedResult.stagePaths, stagedResult.staged);
491
+ }
492
+ else if (stagedResult.stagedPaths.length > 0) {
493
+ io.printf("\nUpdated installer-managed projections for the current framework deposit:\n");
494
+ io.printf(` git add ${stagedResult.stagedPaths.join(" ")}\n`);
495
+ }
496
+ }
497
+ printRefreshSideEffects(io, { ...effects, payloadSwapped: !alreadyCurrent });
408
498
  if (versionSkewNotice) {
409
499
  io.printf(`${versionSkewNotice}\n`);
410
500
  }
@@ -414,6 +504,8 @@ export async function runRefreshDeposit(args, io, seams = {}) {
414
504
  contentVersion,
415
505
  engineVersion,
416
506
  previousDepositVersion,
507
+ alreadyCurrent,
508
+ strategy,
417
509
  agentsMdUpdated,
418
510
  versionSkewNotice,
419
511
  legacyLayout: false,
@@ -515,7 +607,9 @@ export async function runRefreshDepositCli(options) {
515
607
  }
516
608
  try {
517
609
  const result = await runRefreshDeposit(options, io, options.seams);
518
- const state = classification?.state;
610
+ const state = result.alreadyCurrent
611
+ ? "current"
612
+ : classification?.state;
519
613
  if (options.jsonOut) {
520
614
  options.writeOut(`${JSON.stringify(buildUpdateSummaryJson(result, options, state), null, 2)}\n`);
521
615
  printUpdateComplete(result, { printf: options.writeErr }, state);
@@ -61,6 +61,27 @@ export interface GitHooksSeams {
61
61
  setHooksPath?: (projectDir: string, value: string) => boolean;
62
62
  }
63
63
  export declare function writeConsumerGitHooks(projectDir: string, deftDir: string, io: InitDepositIo, seams?: GitHooksSeams): boolean;
64
+ /** Commit SHA for `actions/checkout` deposited in deft-core-guard.yml (#1672 / #1072). */
65
+ export declare const CORE_GUARD_CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
66
+ /** Tag comment paired with {@link CORE_GUARD_CHECKOUT_SHA}. */
67
+ export declare const CORE_GUARD_CHECKOUT_TAG = "v7.0.0";
68
+ /**
69
+ * Extract the `uses: actions/checkout@…` step line from a guard workflow, if present.
70
+ * Uses linear string scanning (no regex) to avoid CodeQL js/polynomial-redos (#1672).
71
+ */
72
+ export declare function extractCoreGuardCheckoutUsesLine(content: string): string | null;
73
+ /**
74
+ * Whether refresh should keep the existing checkout pin instead of the template pin.
75
+ * Legacy framework-deposited `@v4` tags migrate forward; consumer/Dependabot bumps do not.
76
+ */
77
+ export declare function shouldPreserveCoreGuardCheckoutPin(existingUsesLine: string, desiredUsesLine: string): boolean;
78
+ /** Canonical SHA-pinned checkout step for the deposited deft-core-guard workflow. */
79
+ export declare function coreGuardCheckoutUsesLine(sha?: string, tag?: string): string;
80
+ /**
81
+ * On refresh, preserve an existing consumer `actions/checkout@…` pin while updating
82
+ * the managed guard script / allowlist body (#1672 option 1).
83
+ */
84
+ export declare function mergeCoreGuardWorkflowRefresh(existing: string, desired: string): string;
64
85
  export declare function ensureGitattributes(projectDir: string, io: InitDepositIo): boolean;
65
86
  export declare function ensureGreptileIgnore(projectDir: string, io: InitDepositIo): boolean;
66
87
  export declare function ensureCodeqlPathsIgnore(projectDir: string, io: InitDepositIo): boolean;
@@ -22,6 +22,7 @@ const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
22
22
  const VENDORED_TS_PACKAGES_REL = ".deft/core/packages";
23
23
  const VENDORED_TS_TEST_RE = /\.(test|spec)\.(c|m)?[jt]sx?$/i;
24
24
  const CORE_GITATTRIBUTES_LINES = [
25
+ `${CORE_GLOB} text eol=lf`,
25
26
  `${CORE_GLOB} linguist-generated=true`,
26
27
  `${CORE_GLOB} linguist-vendored=true`,
27
28
  ];
@@ -537,6 +538,78 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
537
538
  function githubActionsExpr(expression) {
538
539
  return ["$", "{{ ", expression, " }}"].join("");
539
540
  }
541
+ /** Commit SHA for `actions/checkout` deposited in deft-core-guard.yml (#1672 / #1072). */
542
+ export const CORE_GUARD_CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
543
+ /** Tag comment paired with {@link CORE_GUARD_CHECKOUT_SHA}. */
544
+ export const CORE_GUARD_CHECKOUT_TAG = "v7.0.0";
545
+ const CHECKOUT_USES_PREFIX = "- uses: actions/checkout@";
546
+ /**
547
+ * Extract the `uses: actions/checkout@…` step line from a guard workflow, if present.
548
+ * Uses linear string scanning (no regex) to avoid CodeQL js/polynomial-redos (#1672).
549
+ */
550
+ export function extractCoreGuardCheckoutUsesLine(content) {
551
+ for (const line of content.split("\n")) {
552
+ const trimmed = line.trimStart();
553
+ if (!trimmed.startsWith(CHECKOUT_USES_PREFIX))
554
+ continue;
555
+ // YAML step lines are indented; a bare top-level match is not a step.
556
+ if (trimmed.length === line.length)
557
+ continue;
558
+ return line;
559
+ }
560
+ return null;
561
+ }
562
+ /** Ref after `actions/checkout@` (tag, SHA, or major), stopping at whitespace. */
563
+ function checkoutActionRef(usesLine) {
564
+ const idx = usesLine.indexOf(CHECKOUT_USES_PREFIX);
565
+ if (idx < 0)
566
+ return null;
567
+ const after = usesLine.slice(idx + CHECKOUT_USES_PREFIX.length);
568
+ let end = after.length;
569
+ for (let i = 0; i < after.length; i += 1) {
570
+ const c = after[i];
571
+ if (c === " " || c === "\t" || c === "\r") {
572
+ end = i;
573
+ break;
574
+ }
575
+ }
576
+ const ref = after.slice(0, end);
577
+ return ref.length > 0 ? ref : null;
578
+ }
579
+ /**
580
+ * Whether refresh should keep the existing checkout pin instead of the template pin.
581
+ * Legacy framework-deposited `@v4` tags migrate forward; consumer/Dependabot bumps do not.
582
+ */
583
+ export function shouldPreserveCoreGuardCheckoutPin(existingUsesLine, desiredUsesLine) {
584
+ if (existingUsesLine === desiredUsesLine)
585
+ return false;
586
+ const existingRef = checkoutActionRef(existingUsesLine);
587
+ if (!existingRef)
588
+ return false;
589
+ // Stale framework-deposited floating major tag — migrate to SHA template (#1672).
590
+ return existingRef !== "v4";
591
+ }
592
+ /** Canonical SHA-pinned checkout step for the deposited deft-core-guard workflow. */
593
+ export function coreGuardCheckoutUsesLine(sha = CORE_GUARD_CHECKOUT_SHA, tag = CORE_GUARD_CHECKOUT_TAG) {
594
+ return ` - uses: actions/checkout@${sha} # ${tag}`;
595
+ }
596
+ /**
597
+ * On refresh, preserve an existing consumer `actions/checkout@…` pin while updating
598
+ * the managed guard script / allowlist body (#1672 option 1).
599
+ */
600
+ export function mergeCoreGuardWorkflowRefresh(existing, desired) {
601
+ const existingCheckout = extractCoreGuardCheckoutUsesLine(existing);
602
+ if (!existingCheckout)
603
+ return desired;
604
+ const desiredCheckout = extractCoreGuardCheckoutUsesLine(desired);
605
+ if (!desiredCheckout || !shouldPreserveCoreGuardCheckoutPin(existingCheckout, desiredCheckout)) {
606
+ return desired;
607
+ }
608
+ const idx = desired.indexOf(desiredCheckout);
609
+ if (idx < 0)
610
+ return desired;
611
+ return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
612
+ }
540
613
  function coreGuardWorkflowContent() {
541
614
  const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
542
615
  const headSha = githubActionsExpr("github.event.pull_request.head.sha");
@@ -554,7 +627,7 @@ function coreGuardWorkflowContent() {
554
627
  " no-mixed-core-and-app:\n" +
555
628
  " runs-on: ubuntu-latest\n" +
556
629
  " steps:\n" +
557
- " - uses: actions/checkout@v4\n" +
630
+ `${coreGuardCheckoutUsesLine()}\n` +
558
631
  " with:\n" +
559
632
  " fetch-depth: 0\n" +
560
633
  " - name: Refuse PRs that mix .deft/core/** with non-framework paths\n" +
@@ -584,7 +657,7 @@ export function ensureGitattributes(projectDir, io) {
584
657
  const present = new Set(existing.split("\n").map((line) => line.trim()));
585
658
  const additions = CORE_GITATTRIBUTES_LINES.filter((line) => !present.has(line));
586
659
  if (additions.length === 0) {
587
- io.printf(`.gitattributes already marks ${CORE_GLOB} as generated/vendored — skipping.\n`);
660
+ io.printf(`.gitattributes already marks ${CORE_GLOB} as LF-pinned/generated/vendored — skipping.\n`);
588
661
  return false;
589
662
  }
590
663
  let body = existing;
@@ -594,13 +667,13 @@ export function ensureGitattributes(projectDir, io) {
594
667
  body += "\n";
595
668
  body +=
596
669
  "# Deft framework: the vendored payload is packaged framework code, not\n" +
597
- "# consumer source. Mark it generated + vendored so language stats and\n" +
598
- "# diffs treat .deft/core/** as machine-managed (#1430).\n";
670
+ "# consumer source. Pin LF endings and mark it generated + vendored so\n" +
671
+ "# Git does not rewrite it and diffs treat .deft/core/** as machine-managed (#1430, #2118).\n";
599
672
  for (const add of additions) {
600
673
  body += `${add}\n`;
601
674
  }
602
675
  writeFileSync(path, body, "utf8");
603
- io.printf(`.gitattributes updated with linguist markers: ${additions.join(", ")}\n`);
676
+ io.printf(`.gitattributes updated with Deft core markers: ${additions.join(", ")}\n`);
604
677
  return true;
605
678
  }
606
679
  function greptilePatternPresent(patterns, glob) {
@@ -714,15 +787,16 @@ export function ensureCoreGuardWorkflow(projectDir, io) {
714
787
  const desired = coreGuardWorkflowContent();
715
788
  if (existsSync(path)) {
716
789
  const existing = readFileSync(path, "utf8");
717
- if (existing === desired) {
718
- io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
719
- return false;
720
- }
721
790
  if (!existing.includes("name: deft-core-guard")) {
722
791
  io.printf(`${CORE_GUARD_WORKFLOW_REL} present but not deft-managed — leaving unchanged.\n`);
723
792
  return false;
724
793
  }
725
- writeFileSync(path, desired, "utf8");
794
+ const refreshed = mergeCoreGuardWorkflowRefresh(existing, desired);
795
+ if (existing === refreshed) {
796
+ io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
797
+ return false;
798
+ }
799
+ writeFileSync(path, refreshed, "utf8");
726
800
  io.printf(`${CORE_GUARD_WORKFLOW_REL} refreshed: deft-core-guard allowlist updated (#1478).\n`);
727
801
  return true;
728
802
  }
@@ -1,4 +1,5 @@
1
1
  import type { PolicyResult } from "./resolve.js";
2
+ export { policyColonInvocation, policySetInvocation } from "./policy-invocation.js";
2
3
  /** One-liner disclosure phrasing for AGENTS.md / setup interview echo. */
3
4
  export declare function disclosureLine(result: PolicyResult): string;
4
5
  //# sourceMappingURL=disclosure.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { ENV_BYPASS } from "./resolve.js";
2
+ export { policyColonInvocation, policySetInvocation } from "./policy-invocation.js";
2
3
  /** One-liner disclosure phrasing for AGENTS.md / setup interview echo. */
3
4
  export function disclosureLine(result) {
4
5
  if (result.allowDirectCommits) {
@@ -4,6 +4,7 @@ export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
6
  export * from "./plan-extensions.js";
7
+ export * from "./policy-invocation.js";
7
8
  export * from "./resolve.js";
8
9
  export * from "./value-feedback.js";
9
10
  export * from "./wip.js";
@@ -8,6 +8,7 @@ export * from "./capacity.js";
8
8
  export * from "./decisions.js";
9
9
  export * from "./disclosure.js";
10
10
  export * from "./plan-extensions.js";
11
+ export * from "./policy-invocation.js";
11
12
  export * from "./resolve.js";
12
13
  export * from "./value-feedback.js";
13
14
  export * from "./wip.js";
@@ -0,0 +1,5 @@
1
+ /** User-facing colon-form policy verb for consumer `deft` disclosures (#2367). */
2
+ export declare function policyColonInvocation(subcommand: string, trailing?: string): string;
3
+ /** User-facing `deft policy set <cmd>` form for policy-set disclosures (#2367). */
4
+ export declare function policySetInvocation(subcommand: string, trailing?: string): string;
5
+ //# sourceMappingURL=policy-invocation.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** User-facing colon-form policy verb for consumer `deft` disclosures (#2367). */
2
+ export function policyColonInvocation(subcommand, trailing = "") {
3
+ return `deft policy:${subcommand}${trailing}`;
4
+ }
5
+ /** User-facing `deft policy set <cmd>` form for policy-set disclosures (#2367). */
6
+ export function policySetInvocation(subcommand, trailing = "") {
7
+ return `deft policy set ${subcommand}${trailing}`;
8
+ }
9
+ //# sourceMappingURL=policy-invocation.js.map
@@ -3,6 +3,7 @@ import { join, resolve as pathResolve } from "node:path";
3
3
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
4
4
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
5
5
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
6
+ import { policyColonInvocation } from "./policy-invocation.js";
6
7
  /** Filesystem-relative location of the project-definition xBRIEF (display/back-compat). */
7
8
  export const PROJECT_DEFINITION_REL_PATH = "xbrief/PROJECT-DEFINITION.xbrief.json";
8
9
  /** Environment variable emergency bypass for branch protection (#747). */
@@ -154,8 +155,9 @@ export function resolvePolicy(projectRoot) {
154
155
  const warn = `DEPRECATED: PROJECT-DEFINITION uses the legacy narrative key ` +
155
156
  `'${LEGACY_NARRATIVE_KEY}' (${pythonRepr(raw)}). Migrate to typed ` +
156
157
  `plan.policy.allowDirectCommitsToMaster (#746). Run ` +
157
- `\`task policy:enforce-branches\` or \`task policy:allow-direct-commits ` +
158
- `-- --confirm\` to set the typed flag explicitly.`;
158
+ `\`${policyColonInvocation("enforce-branches")}\` or ` +
159
+ `\`${policyColonInvocation("allow-direct-commits", " -- --confirm")}\` ` +
160
+ `to set the typed flag explicitly.`;
159
161
  return {
160
162
  allowDirectCommits: allow,
161
163
  source: "legacy-narrative",
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
3
3
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
4
+ import { policyColonInvocation } from "./policy-invocation.js";
4
5
  import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
5
6
  import { isTrustedOrgAutoEnable } from "./value-feedback-autoenable.js";
6
7
  /** Canonical registered policy field name (matches other FIELD_* dotted paths). */
@@ -21,7 +22,9 @@ export const VALUE_FEEDBACK_CAPABILITY_COST_DISCLOSURE = "\u26a0 Capability-cost
21
22
  " \u2022 A budgeted session one-liner may appear when concrete attributed value exists.\n" +
22
23
  " \u2022 Upstream gap-escalation prompts stay OFF unless you explicitly enable " +
23
24
  "`upstreamPrompt` (GitHub attention + token cost).\n" +
24
- " \u2022 Inspect current state: `task policy:show --field=valueFeedback`.\n" +
25
+ " \u2022 Inspect current state: `" +
26
+ policyColonInvocation("show", " --field=valueFeedback") +
27
+ "`.\n" +
25
28
  " \u2022 Reversible: set `enabled: false` under the typed policy block in PROJECT-DEFINITION.\n" +
26
29
  " \u2022 Changes are recorded to meta/policy-changes.log for auditability.";
27
30
  function defaultResolved(source, error = null) {
@@ -202,7 +205,7 @@ export function enableValueFeedback(projectRoot, options) {
202
205
  return {
203
206
  exitCode: 1,
204
207
  stdout: `${VALUE_FEEDBACK_CAPABILITY_COST_DISCLOSURE}\n\n` +
205
- "Re-run with --confirm to apply: task policy:enable-value-feedback -- --confirm\n",
208
+ `Re-run with --confirm to apply: ${policyColonInvocation("enable-value-feedback", " -- --confirm")}\n`,
206
209
  changed: false,
207
210
  };
208
211
  }
@@ -265,7 +268,7 @@ export function enableValueFeedback(projectRoot, options) {
265
268
  if (changedFlag) {
266
269
  atomicWriteProjectDefinition(path, data);
267
270
  }
268
- const actor = options.actor ?? "task policy:enable-value-feedback";
271
+ const actor = options.actor ?? policyColonInvocation("enable-value-feedback");
269
272
  const note = options.note ?? "";
270
273
  const parts = [
271
274
  `actor=${actor}`,
@@ -3,6 +3,10 @@ export declare const EXIT_MERGE_BLOCKED = 1;
3
3
  export declare const EXIT_EXTERNAL_ERROR = 2;
4
4
  export declare const GREPTILE_LOGIN = "greptile-apps[bot]";
5
5
  export declare const GREPTILE_ERRORED_SENTINEL = "Greptile encountered an error while reviewing this PR";
6
+ /** Greptile status marker on excluded-author skip comments (#2375). */
7
+ export declare const GREPTILE_STATUS_MARKER = "<!-- greptile-status -->";
8
+ /** Substring Greptile posts when the PR author is on the excluded-authors list (#2375). */
9
+ export declare const GREPTILE_EXCLUDED_AUTHOR_PHRASE = "excluded authors list";
6
10
  export declare const LAST_REVIEWED_RE: RegExp;
7
11
  export declare const CONFIDENCE_RE: RegExp;
8
12
  export declare const P0_BADGE = "<img alt=\"P0\"";
@@ -3,6 +3,10 @@ export const EXIT_MERGE_BLOCKED = 1;
3
3
  export const EXIT_EXTERNAL_ERROR = 2;
4
4
  export const GREPTILE_LOGIN = "greptile-apps[bot]";
5
5
  export const GREPTILE_ERRORED_SENTINEL = "Greptile encountered an error while reviewing this PR";
6
+ /** Greptile status marker on excluded-author skip comments (#2375). */
7
+ export const GREPTILE_STATUS_MARKER = "<!-- greptile-status -->";
8
+ /** Substring Greptile posts when the PR author is on the excluded-authors list (#2375). */
9
+ export const GREPTILE_EXCLUDED_AUTHOR_PHRASE = "excluded authors list";
6
10
  export const LAST_REVIEWED_RE = /Last reviewed commit:\s*\[[^\]]*\]\(https?:\/\/github\.com\/[^/]+\/[^/]+\/commit\/(?<sha>[0-9a-f]{7,40})/g;
7
11
  export const CONFIDENCE_RE = /Confidence Score:\s*(?<score>\d+)\s*\/\s*5/i;
8
12
  export const P0_BADGE = '<img alt="P0"';
@@ -8,6 +8,9 @@ export function evaluateGates(_prNumber, headSha, verdict) {
8
8
  "Wait for the review to land before merging (see #796 late-bot-review re-check).");
9
9
  return failures;
10
10
  }
11
+ if (verdict.excludedAuthor) {
12
+ return failures;
13
+ }
11
14
  if (verdict.errored) {
12
15
  failures.push("Greptile review is in the ERRORED state on the current HEAD (#526). " +
13
16
  "Retry via @greptileai or escalate per " +
@@ -31,13 +31,15 @@ export declare function verdictShaIsStale(verdict: GreptileVerdict, headSha: str
31
31
  * Classify whether the verdict-based merge block is ONLY "soft" (#2260).
32
32
  *
33
33
  * A soft block means the review verdict is absent or pinned to a prior head
34
- * SHA (rebased staleness) -- i.e. the review has not spoken about the CURRENT
35
- * head. It is safe to reconcile a soft block against GitHub mergeability.
34
+ * SHA without carrying real blocker-class findings (rebased staleness with no
35
+ * P0/P1/errored/low-confidence) -- i.e. the review has not spoken about the
36
+ * CURRENT head in a blocking way. It is safe to reconcile a soft block against
37
+ * GitHub mergeability.
36
38
  *
37
- * A HARD block -- a genuine P0/P1 finding, an ERRORED review, or a low
38
- * confidence score on the current head -- is NEVER soft; those must keep
39
- * blocking regardless of GitHub mergeability (guardrail: do not merge a PR
40
- * with a real P0/P1 review finding).
39
+ * A HARD block -- a genuine P0/P1 finding (even on a stale SHA), an ERRORED
40
+ * review, or a low confidence score -- is NEVER soft; those must keep blocking
41
+ * regardless of GitHub mergeability (guardrail: do not merge a PR with a real
42
+ * P0/P1 review finding).
41
43
  */
42
44
  export declare function verdictBlockIsSoftOnly(verdict: GreptileVerdict, headSha: string | null): boolean;
43
45
  //# sourceMappingURL=mergeability.d.ts.map