@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.
- package/dist/branch/evaluate.js +2 -1
- package/dist/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/deposit/copy-tree.js +31 -7
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/main.js +2 -1
- package/dist/fs/projection-containment.d.ts +35 -0
- package/dist/fs/projection-containment.js +118 -0
- package/dist/init-deposit/hygiene.d.ts +6 -0
- package/dist/init-deposit/hygiene.js +5 -1
- package/dist/init-deposit/refresh.d.ts +16 -2
- package/dist/init-deposit/refresh.js +144 -50
- package/dist/init-deposit/scaffold.d.ts +21 -0
- package/dist/init-deposit/scaffold.js +84 -10
- package/dist/policy/disclosure.d.ts +1 -0
- package/dist/policy/disclosure.js +1 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/policy-invocation.d.ts +5 -0
- package/dist/policy/policy-invocation.js +9 -0
- package/dist/policy/resolve.js +4 -2
- package/dist/policy/value-feedback.js +6 -3
- package/dist/pr-merge-readiness/constants.d.ts +4 -0
- package/dist/pr-merge-readiness/constants.js +4 -0
- package/dist/pr-merge-readiness/evaluate.js +3 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +8 -6
- package/dist/pr-merge-readiness/mergeability.js +15 -10
- package/dist/pr-merge-readiness/output.js +12 -6
- package/dist/pr-merge-readiness/parse.d.ts +1 -0
- package/dist/pr-merge-readiness/parse.js +9 -1
- package/dist/pr-merge-readiness/types.d.ts +2 -0
- package/dist/preflight-cache/evaluate.d.ts +2 -0
- package/dist/preflight-cache/evaluate.js +14 -3
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +2 -0
- package/dist/release/preflight.d.ts +2 -0
- package/dist/release/preflight.js +14 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/main.js +24 -1
- package/dist/scope/open-umbrella-warning.d.ts +12 -0
- package/dist/scope/open-umbrella-warning.js +403 -0
- package/dist/swarm/constants.d.ts +1 -1
- package/dist/swarm/constants.js +2 -1
- package/dist/swarm/subagent-backend.js +2 -1
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/ts-check-lane/run-lane.d.ts +4 -0
- package/dist/ts-check-lane/run-lane.js +20 -6
- package/dist/value/feedback-file.js +3 -2
- package/dist/value/readback.js +3 -2
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- 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
|
|
222
|
-
const
|
|
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
|
-
|
|
235
|
+
entries.push({ status, path: trimmed.replace(/\\/g, "/") });
|
|
234
236
|
}
|
|
235
|
-
return
|
|
237
|
+
return entries;
|
|
236
238
|
}
|
|
237
|
-
function
|
|
239
|
+
function classifyChangedEntries(changed) {
|
|
238
240
|
const core = [];
|
|
239
241
|
const installerManaged = [];
|
|
240
|
-
for (const
|
|
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(
|
|
247
|
+
core.push(entry);
|
|
245
248
|
}
|
|
246
249
|
else if (isInstallerManagedPath(path)) {
|
|
247
|
-
installerManaged.push(
|
|
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,
|
|
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 =
|
|
258
|
-
const { core, installerManaged } =
|
|
259
|
-
const
|
|
260
|
-
|
|
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,
|
|
263
|
-
if (
|
|
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:
|
|
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(
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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) {
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/policy/index.js
CHANGED
|
@@ -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
|
package/dist/policy/resolve.js
CHANGED
|
@@ -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
|
-
|
|
158
|
-
|
|
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: `
|
|
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
|
-
|
|
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 ?? "
|
|
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
|
|
35
|
-
*
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|