@deftai/directive-core 0.73.1 → 0.75.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/agents-md-budget/evaluate.d.ts +61 -0
- package/dist/agents-md-budget/evaluate.js +321 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +33 -0
- package/dist/agents-md-budget/skill-frontmatter.js +115 -0
- package/dist/check/orchestrator.d.ts +3 -0
- package/dist/check/orchestrator.js +2 -1
- package/dist/codebase/map.js +2 -0
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/checks.d.ts +9 -0
- package/dist/doctor/checks.js +67 -0
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/main.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- package/dist/fs/projection-containment.d.ts +35 -0
- package/dist/fs/projection-containment.js +118 -0
- package/dist/init-deposit/scaffold.d.ts +21 -0
- package/dist/init-deposit/scaffold.js +86 -9
- package/dist/intake/issue-emit.js +2 -2
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- 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/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- 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/scope/vbrief-ref.js +3 -3
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +69 -58
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/vbrief-reconcile/umbrellas.js +12 -11
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/dist/xbrief-migrate/migrate-project.js +9 -0
- package/package.json +7 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* projection-containment.ts — repo-controlled projection symlink guard (#2413).
|
|
3
|
+
*
|
|
4
|
+
* Projection writers (`codebase:map`, `triage:bootstrap`) write to well-known
|
|
5
|
+
* paths such as `.planning/codebase/MAP.md`, `.gitignore`, and `.gitattributes`.
|
|
6
|
+
* If a malicious repo author commits one of those paths (or a parent) as a
|
|
7
|
+
* symlink escaping the project tree, routine operator commands would follow the
|
|
8
|
+
* link and create/overwrite files outside the checkout.
|
|
9
|
+
*
|
|
10
|
+
* `assertProjectionContained` anchors on `realpath(projectDir)`, walks each
|
|
11
|
+
* existing component down to the write target, rejects symlinks that escape the
|
|
12
|
+
* tree, and asserts the deepest existing ancestor stays under the project root.
|
|
13
|
+
*
|
|
14
|
+
* Refs #2413.
|
|
15
|
+
*/
|
|
16
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
17
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
18
|
+
/** Non-zero exit code for a projection-containment refusal (needs-action). */
|
|
19
|
+
export const PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
20
|
+
/** Thrown when a projection write target escapes the project tree. */
|
|
21
|
+
export class ProjectionContainmentError extends Error {
|
|
22
|
+
projectDir;
|
|
23
|
+
targetPath;
|
|
24
|
+
offendingPath;
|
|
25
|
+
constructor(message, details) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "ProjectionContainmentError";
|
|
28
|
+
this.projectDir = details.projectDir;
|
|
29
|
+
this.targetPath = details.targetPath;
|
|
30
|
+
this.offendingPath = details.offendingPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Path-SEGMENT containment: is `child` equal to `parent` or nested under it?
|
|
35
|
+
* Uses `path.relative` so `/foo` is NOT treated as containing `/foobar`.
|
|
36
|
+
*/
|
|
37
|
+
function isContained(parent, child) {
|
|
38
|
+
if (parent === child) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const rel = relative(parent, child);
|
|
42
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Refuse projection writes when the target (or a parent) is a symlink that
|
|
46
|
+
* escapes the resolved project tree. Throws {@link ProjectionContainmentError};
|
|
47
|
+
* MUST be called BEFORE any projection read/write/mkdir.
|
|
48
|
+
*/
|
|
49
|
+
export function assertProjectionContained(projectDir, targetPath) {
|
|
50
|
+
const projectAbs = resolve(projectDir);
|
|
51
|
+
let projectReal;
|
|
52
|
+
try {
|
|
53
|
+
projectReal = realpathSync(projectAbs);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new ProjectionContainmentError(`projection write refused: project directory ${projectAbs} does not exist`, { projectDir: projectAbs, targetPath: resolve(targetPath), offendingPath: projectAbs });
|
|
57
|
+
}
|
|
58
|
+
const targetAbs = resolve(targetPath);
|
|
59
|
+
const rel = relative(projectAbs, targetAbs);
|
|
60
|
+
if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel)) {
|
|
61
|
+
throw new ProjectionContainmentError(`projection write refused: target ${targetAbs} is not nested under the project tree ${projectAbs}`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: targetAbs });
|
|
62
|
+
}
|
|
63
|
+
const segments = rel.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
|
64
|
+
let current = projectAbs;
|
|
65
|
+
let deepestExistingReal = projectReal;
|
|
66
|
+
for (const segment of segments) {
|
|
67
|
+
current = join(current, segment);
|
|
68
|
+
let info;
|
|
69
|
+
try {
|
|
70
|
+
info = lstatSync(current);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
if (info.isSymbolicLink()) {
|
|
76
|
+
let linkReal;
|
|
77
|
+
try {
|
|
78
|
+
linkReal = realpathSync(current);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new ProjectionContainmentError(`projection write refused: ${current} is a broken/dangling symlink on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
82
|
+
}
|
|
83
|
+
if (!isContained(projectReal, linkReal)) {
|
|
84
|
+
throw new ProjectionContainmentError(`projection write refused: ${current} is a symlink escaping the project tree ` +
|
|
85
|
+
`(resolves to ${linkReal}, outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
86
|
+
}
|
|
87
|
+
deepestExistingReal = linkReal;
|
|
88
|
+
current = linkReal;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
try {
|
|
92
|
+
deepestExistingReal = realpathSync(current);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
throw new ProjectionContainmentError(`projection write refused: could not resolve ${current} on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const targetReal = realpathSync(targetAbs);
|
|
101
|
+
if (!isContained(projectReal, targetReal)) {
|
|
102
|
+
throw new ProjectionContainmentError(`projection write refused: projection target resolves outside the project tree ` +
|
|
103
|
+
`(${targetReal} is outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: targetReal });
|
|
104
|
+
}
|
|
105
|
+
deepestExistingReal = targetReal;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof ProjectionContainmentError) {
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
// Target does not exist yet; parent walk above is sufficient.
|
|
112
|
+
}
|
|
113
|
+
if (!isContained(projectReal, deepestExistingReal)) {
|
|
114
|
+
throw new ProjectionContainmentError(`projection write refused: projection path escapes the project tree ` +
|
|
115
|
+
`(${deepestExistingReal} is outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: deepestExistingReal });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=projection-containment.js.map
|
|
@@ -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;
|
|
@@ -457,7 +457,9 @@ export function ensureTaskfile(projectDir, io) {
|
|
|
457
457
|
return true;
|
|
458
458
|
}
|
|
459
459
|
const HOOK_FILENAMES = ["pre-commit", "pre-push"];
|
|
460
|
+
const HOOK_SUPPORT_FILENAMES = ["_deft-run.sh"];
|
|
460
461
|
const HOOK_FILE_MODE = 0o755;
|
|
462
|
+
const HOOK_SUPPORT_FILE_MODE = 0o644;
|
|
461
463
|
export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
462
464
|
const srcDir = join(deftDir, ".githooks");
|
|
463
465
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
@@ -467,18 +469,20 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
467
469
|
const dstDir = join(projectDir, ".githooks");
|
|
468
470
|
mkdirSync(dstDir, { recursive: true });
|
|
469
471
|
let filesDeposited = false;
|
|
470
|
-
for (const name of HOOK_FILENAMES) {
|
|
472
|
+
for (const name of [...HOOK_FILENAMES, ...HOOK_SUPPORT_FILENAMES]) {
|
|
471
473
|
const src = join(srcDir, name);
|
|
472
474
|
if (!existsSync(src))
|
|
473
475
|
continue;
|
|
474
476
|
const data = readFileSync(src);
|
|
475
477
|
const dst = join(dstDir, name);
|
|
476
478
|
const existing = existsSync(dst) ? readFileSync(dst) : null;
|
|
479
|
+
const isHookScript = HOOK_FILENAMES.includes(name);
|
|
477
480
|
if (!existing?.equals(data)) {
|
|
478
|
-
|
|
481
|
+
const mode = isHookScript ? HOOK_FILE_MODE : HOOK_SUPPORT_FILE_MODE;
|
|
482
|
+
writeFileSync(dst, data, { mode });
|
|
479
483
|
filesDeposited = true;
|
|
480
484
|
}
|
|
481
|
-
if (platform() !== "win32") {
|
|
485
|
+
if (platform() !== "win32" && isHookScript) {
|
|
482
486
|
try {
|
|
483
487
|
const mode = statSync(dst).mode & 0o777;
|
|
484
488
|
if ((mode & 0o111) === 0) {
|
|
@@ -538,6 +542,78 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
538
542
|
function githubActionsExpr(expression) {
|
|
539
543
|
return ["$", "{{ ", expression, " }}"].join("");
|
|
540
544
|
}
|
|
545
|
+
/** Commit SHA for `actions/checkout` deposited in deft-core-guard.yml (#1672 / #1072). */
|
|
546
|
+
export const CORE_GUARD_CHECKOUT_SHA = "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0";
|
|
547
|
+
/** Tag comment paired with {@link CORE_GUARD_CHECKOUT_SHA}. */
|
|
548
|
+
export const CORE_GUARD_CHECKOUT_TAG = "v7.0.0";
|
|
549
|
+
const CHECKOUT_USES_PREFIX = "- uses: actions/checkout@";
|
|
550
|
+
/**
|
|
551
|
+
* Extract the `uses: actions/checkout@…` step line from a guard workflow, if present.
|
|
552
|
+
* Uses linear string scanning (no regex) to avoid CodeQL js/polynomial-redos (#1672).
|
|
553
|
+
*/
|
|
554
|
+
export function extractCoreGuardCheckoutUsesLine(content) {
|
|
555
|
+
for (const line of content.split("\n")) {
|
|
556
|
+
const trimmed = line.trimStart();
|
|
557
|
+
if (!trimmed.startsWith(CHECKOUT_USES_PREFIX))
|
|
558
|
+
continue;
|
|
559
|
+
// YAML step lines are indented; a bare top-level match is not a step.
|
|
560
|
+
if (trimmed.length === line.length)
|
|
561
|
+
continue;
|
|
562
|
+
return line;
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
/** Ref after `actions/checkout@` (tag, SHA, or major), stopping at whitespace. */
|
|
567
|
+
function checkoutActionRef(usesLine) {
|
|
568
|
+
const idx = usesLine.indexOf(CHECKOUT_USES_PREFIX);
|
|
569
|
+
if (idx < 0)
|
|
570
|
+
return null;
|
|
571
|
+
const after = usesLine.slice(idx + CHECKOUT_USES_PREFIX.length);
|
|
572
|
+
let end = after.length;
|
|
573
|
+
for (let i = 0; i < after.length; i += 1) {
|
|
574
|
+
const c = after[i];
|
|
575
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
576
|
+
end = i;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const ref = after.slice(0, end);
|
|
581
|
+
return ref.length > 0 ? ref : null;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Whether refresh should keep the existing checkout pin instead of the template pin.
|
|
585
|
+
* Legacy framework-deposited `@v4` tags migrate forward; consumer/Dependabot bumps do not.
|
|
586
|
+
*/
|
|
587
|
+
export function shouldPreserveCoreGuardCheckoutPin(existingUsesLine, desiredUsesLine) {
|
|
588
|
+
if (existingUsesLine === desiredUsesLine)
|
|
589
|
+
return false;
|
|
590
|
+
const existingRef = checkoutActionRef(existingUsesLine);
|
|
591
|
+
if (!existingRef)
|
|
592
|
+
return false;
|
|
593
|
+
// Stale framework-deposited floating major tag — migrate to SHA template (#1672).
|
|
594
|
+
return existingRef !== "v4";
|
|
595
|
+
}
|
|
596
|
+
/** Canonical SHA-pinned checkout step for the deposited deft-core-guard workflow. */
|
|
597
|
+
export function coreGuardCheckoutUsesLine(sha = CORE_GUARD_CHECKOUT_SHA, tag = CORE_GUARD_CHECKOUT_TAG) {
|
|
598
|
+
return ` - uses: actions/checkout@${sha} # ${tag}`;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* On refresh, preserve an existing consumer `actions/checkout@…` pin while updating
|
|
602
|
+
* the managed guard script / allowlist body (#1672 option 1).
|
|
603
|
+
*/
|
|
604
|
+
export function mergeCoreGuardWorkflowRefresh(existing, desired) {
|
|
605
|
+
const existingCheckout = extractCoreGuardCheckoutUsesLine(existing);
|
|
606
|
+
if (!existingCheckout)
|
|
607
|
+
return desired;
|
|
608
|
+
const desiredCheckout = extractCoreGuardCheckoutUsesLine(desired);
|
|
609
|
+
if (!desiredCheckout || !shouldPreserveCoreGuardCheckoutPin(existingCheckout, desiredCheckout)) {
|
|
610
|
+
return desired;
|
|
611
|
+
}
|
|
612
|
+
const idx = desired.indexOf(desiredCheckout);
|
|
613
|
+
if (idx < 0)
|
|
614
|
+
return desired;
|
|
615
|
+
return desired.slice(0, idx) + existingCheckout + desired.slice(idx + desiredCheckout.length);
|
|
616
|
+
}
|
|
541
617
|
function coreGuardWorkflowContent() {
|
|
542
618
|
const baseSha = githubActionsExpr("github.event.pull_request.base.sha");
|
|
543
619
|
const headSha = githubActionsExpr("github.event.pull_request.head.sha");
|
|
@@ -555,7 +631,7 @@ function coreGuardWorkflowContent() {
|
|
|
555
631
|
" no-mixed-core-and-app:\n" +
|
|
556
632
|
" runs-on: ubuntu-latest\n" +
|
|
557
633
|
" steps:\n" +
|
|
558
|
-
|
|
634
|
+
`${coreGuardCheckoutUsesLine()}\n` +
|
|
559
635
|
" with:\n" +
|
|
560
636
|
" fetch-depth: 0\n" +
|
|
561
637
|
" - name: Refuse PRs that mix .deft/core/** with non-framework paths\n" +
|
|
@@ -715,15 +791,16 @@ export function ensureCoreGuardWorkflow(projectDir, io) {
|
|
|
715
791
|
const desired = coreGuardWorkflowContent();
|
|
716
792
|
if (existsSync(path)) {
|
|
717
793
|
const existing = readFileSync(path, "utf8");
|
|
718
|
-
if (existing === desired) {
|
|
719
|
-
io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
794
|
if (!existing.includes("name: deft-core-guard")) {
|
|
723
795
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} present but not deft-managed — leaving unchanged.\n`);
|
|
724
796
|
return false;
|
|
725
797
|
}
|
|
726
|
-
|
|
798
|
+
const refreshed = mergeCoreGuardWorkflowRefresh(existing, desired);
|
|
799
|
+
if (existing === refreshed) {
|
|
800
|
+
io.printf(`${CORE_GUARD_WORKFLOW_REL} already current — skipping.\n`);
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
writeFileSync(path, refreshed, "utf8");
|
|
727
804
|
io.printf(`${CORE_GUARD_WORKFLOW_REL} refreshed: deft-core-guard allowlist updated (#1478).\n`);
|
|
728
805
|
return true;
|
|
729
806
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
5
5
|
import { call } from "../scm/call.js";
|
|
6
6
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
@@ -217,7 +217,7 @@ export function expandPatterns(patterns, root = null) {
|
|
|
217
217
|
const seen = new Set();
|
|
218
218
|
const out = [];
|
|
219
219
|
for (const pattern of patterns) {
|
|
220
|
-
const candidate = root !== null && !pattern
|
|
220
|
+
const candidate = root !== null && !isAbsolute(pattern) ? join(root, pattern) : pattern;
|
|
221
221
|
let matches = globSync(candidate).sort();
|
|
222
222
|
if (matches.length === 0) {
|
|
223
223
|
try {
|
|
@@ -9,9 +9,32 @@
|
|
|
9
9
|
* it is an explicit, reviewed diff to this typed field -- that diff IS the
|
|
10
10
|
* "was this growth deliberate?" checkpoint.
|
|
11
11
|
*/
|
|
12
|
+
export type HarnessProfile = "cursor" | "none";
|
|
13
|
+
export type SkillFrontmatterTier = "daily-core" | "all" | "none";
|
|
12
14
|
export interface AgentsMdBudget {
|
|
13
15
|
readonly managedMaxLines: number;
|
|
14
16
|
readonly unmanagedMaxLines: number;
|
|
17
|
+
/**
|
|
18
|
+
* Fail-closed UTF-8 byte ratchet for the managed section (#2452).
|
|
19
|
+
* Seeded at the current measured size so the gate ships green; growth past
|
|
20
|
+
* this ceiling fails. The <=8192 B north-star is reported separately.
|
|
21
|
+
*/
|
|
22
|
+
readonly absoluteMaxBytes?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Harness profile for DD-3 skill-frontmatter measurement (#2463).
|
|
25
|
+
* When unset, the gate auto-detects `cursor` when `content/skills/` exists.
|
|
26
|
+
*/
|
|
27
|
+
readonly harnessProfile?: HarnessProfile;
|
|
28
|
+
/**
|
|
29
|
+
* Skill tier for DD-3 frontmatter caps (`daily-core` vs `all` vs `none`).
|
|
30
|
+
* Defaults to `all` for reporting when unset.
|
|
31
|
+
*/
|
|
32
|
+
readonly skillFrontmatterTier?: SkillFrontmatterTier;
|
|
33
|
+
/**
|
|
34
|
+
* Optional fail-closed ratchet for DD-3 skill-frontmatter bytes (#2463).
|
|
35
|
+
* Advisory when unset; growth past this ceiling fails when set.
|
|
36
|
+
*/
|
|
37
|
+
readonly skillFrontmatterMaxBytes?: number;
|
|
15
38
|
}
|
|
16
39
|
export type AgentsMdBudgetSource = "typed" | "unset" | "default-on-error";
|
|
17
40
|
export interface AgentsMdBudgetResult {
|
|
@@ -39,6 +39,19 @@ function readRegion(block, key) {
|
|
|
39
39
|
}
|
|
40
40
|
return { value: raw, error: null };
|
|
41
41
|
}
|
|
42
|
+
function readOptionalRegion(block, key) {
|
|
43
|
+
if (!(key in block)) {
|
|
44
|
+
return { value: undefined, error: null };
|
|
45
|
+
}
|
|
46
|
+
const raw = block[key];
|
|
47
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
48
|
+
return {
|
|
49
|
+
value: undefined,
|
|
50
|
+
error: `plan.policy.agentsMdBudget.${key} must be a non-negative integer; got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { value: raw, error: null };
|
|
54
|
+
}
|
|
42
55
|
/** Resolve plan.policy.agentsMdBudget from PROJECT-DEFINITION (#645). */
|
|
43
56
|
export function resolveAgentsMdBudget(projectRoot) {
|
|
44
57
|
const [data, err] = loadProjectDefinition(projectRoot);
|
|
@@ -77,13 +90,71 @@ export function resolveAgentsMdBudget(projectRoot) {
|
|
|
77
90
|
if (unmanaged.error !== null) {
|
|
78
91
|
return { budget: null, source: "default-on-error", error: unmanaged.error };
|
|
79
92
|
}
|
|
93
|
+
const absolute = readOptionalRegion(block, "absoluteMaxBytes");
|
|
94
|
+
if (absolute.error !== null) {
|
|
95
|
+
return { budget: null, source: "default-on-error", error: absolute.error };
|
|
96
|
+
}
|
|
97
|
+
const skillFrontmatterMax = readOptionalRegion(block, "skillFrontmatterMaxBytes");
|
|
98
|
+
if (skillFrontmatterMax.error !== null) {
|
|
99
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterMax.error };
|
|
100
|
+
}
|
|
101
|
+
const harnessProfile = readOptionalHarnessProfile(block, "harnessProfile");
|
|
102
|
+
if (harnessProfile.error !== null) {
|
|
103
|
+
return { budget: null, source: "default-on-error", error: harnessProfile.error };
|
|
104
|
+
}
|
|
105
|
+
const skillFrontmatterTier = readOptionalSkillTier(block, "skillFrontmatterTier");
|
|
106
|
+
if (skillFrontmatterTier.error !== null) {
|
|
107
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterTier.error };
|
|
108
|
+
}
|
|
109
|
+
const budget = {
|
|
110
|
+
managedMaxLines: managed.value,
|
|
111
|
+
unmanagedMaxLines: unmanaged.value,
|
|
112
|
+
};
|
|
113
|
+
const withOptional = {
|
|
114
|
+
...budget,
|
|
115
|
+
...(absolute.value !== undefined ? { absoluteMaxBytes: absolute.value } : {}),
|
|
116
|
+
...(skillFrontmatterMax.value !== undefined
|
|
117
|
+
? { skillFrontmatterMaxBytes: skillFrontmatterMax.value }
|
|
118
|
+
: {}),
|
|
119
|
+
...(harnessProfile.value !== undefined ? { harnessProfile: harnessProfile.value } : {}),
|
|
120
|
+
...(skillFrontmatterTier.value !== undefined
|
|
121
|
+
? { skillFrontmatterTier: skillFrontmatterTier.value }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
80
124
|
return {
|
|
81
|
-
budget:
|
|
82
|
-
managedMaxLines: managed.value,
|
|
83
|
-
unmanagedMaxLines: unmanaged.value,
|
|
84
|
-
},
|
|
125
|
+
budget: withOptional,
|
|
85
126
|
source: "typed",
|
|
86
127
|
error: null,
|
|
87
128
|
};
|
|
88
129
|
}
|
|
130
|
+
const HARNESS_PROFILES = new Set(["cursor", "none"]);
|
|
131
|
+
const SKILL_FRONTMATTER_TIERS = new Set(["daily-core", "all", "none"]);
|
|
132
|
+
function readOptionalHarnessProfile(block, key) {
|
|
133
|
+
if (!(key in block)) {
|
|
134
|
+
return { value: undefined, error: null };
|
|
135
|
+
}
|
|
136
|
+
const raw = block[key];
|
|
137
|
+
if (typeof raw !== "string" || !HARNESS_PROFILES.has(raw)) {
|
|
138
|
+
return {
|
|
139
|
+
value: undefined,
|
|
140
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...HARNESS_PROFILES].join(", ")}; ` +
|
|
141
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { value: raw, error: null };
|
|
145
|
+
}
|
|
146
|
+
function readOptionalSkillTier(block, key) {
|
|
147
|
+
if (!(key in block)) {
|
|
148
|
+
return { value: undefined, error: null };
|
|
149
|
+
}
|
|
150
|
+
const raw = block[key];
|
|
151
|
+
if (typeof raw !== "string" || !SKILL_FRONTMATTER_TIERS.has(raw)) {
|
|
152
|
+
return {
|
|
153
|
+
value: undefined,
|
|
154
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...SKILL_FRONTMATTER_TIERS].join(", ")}; ` +
|
|
155
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { value: raw, error: null };
|
|
159
|
+
}
|
|
89
160
|
//# sourceMappingURL=agents-md-budget.js.map
|
|
@@ -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
|
|
@@ -65,13 +65,15 @@ export function verdictShaIsStale(verdict, headSha) {
|
|
|
65
65
|
* Classify whether the verdict-based merge block is ONLY "soft" (#2260).
|
|
66
66
|
*
|
|
67
67
|
* A soft block means the review verdict is absent or pinned to a prior head
|
|
68
|
-
* SHA
|
|
69
|
-
*
|
|
68
|
+
* SHA without carrying real blocker-class findings (rebased staleness with no
|
|
69
|
+
* P0/P1/errored/low-confidence) -- i.e. the review has not spoken about the
|
|
70
|
+
* CURRENT head in a blocking way. It is safe to reconcile a soft block against
|
|
71
|
+
* GitHub mergeability.
|
|
70
72
|
*
|
|
71
|
-
* A HARD block -- a genuine P0/P1 finding
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* A HARD block -- a genuine P0/P1 finding (even on a stale SHA), an ERRORED
|
|
74
|
+
* review, or a low confidence score -- is NEVER soft; those must keep blocking
|
|
75
|
+
* regardless of GitHub mergeability (guardrail: do not merge a PR with a real
|
|
76
|
+
* P0/P1 review finding).
|
|
75
77
|
*/
|
|
76
78
|
export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
77
79
|
// Absent: no Greptile rolling-summary comment at all.
|
|
@@ -83,12 +85,11 @@ export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
|
83
85
|
if (verdict.informalClean) {
|
|
84
86
|
return false;
|
|
85
87
|
}
|
|
86
|
-
//
|
|
87
|
-
if (
|
|
88
|
+
// Excluded-author skip is an intentional N/A reviewer state (#2375).
|
|
89
|
+
if (verdict.excludedAuthor) {
|
|
88
90
|
return true;
|
|
89
91
|
}
|
|
90
|
-
//
|
|
91
|
-
// finding is a hard block and must not be overridden.
|
|
92
|
+
// Blocker-class signals apply even when the verdict SHA is stale (#2382).
|
|
92
93
|
if (verdict.errored) {
|
|
93
94
|
return false;
|
|
94
95
|
}
|
|
@@ -98,6 +99,10 @@ export function verdictBlockIsSoftOnly(verdict, headSha) {
|
|
|
98
99
|
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
99
100
|
return false;
|
|
100
101
|
}
|
|
102
|
+
// Stale without blocker findings: review has not spoken about the current head.
|
|
103
|
+
if (verdictShaIsStale(verdict, headSha)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
101
106
|
// No genuine finding: the block is a missing-canonical-field / not-yet-posted
|
|
102
107
|
// wait, which GitHub mergeability may resolve.
|
|
103
108
|
return true;
|
|
@@ -10,6 +10,7 @@ function verdictToDict(verdict) {
|
|
|
10
10
|
p1_count: verdict.p1Count,
|
|
11
11
|
p2_count: verdict.p2Count,
|
|
12
12
|
informal_clean: verdict.informalClean,
|
|
13
|
+
excluded_author: verdict.excludedAuthor,
|
|
13
14
|
raw_body_excerpt: verdict.rawBodyExcerpt,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -47,12 +48,17 @@ export function printHuman(result) {
|
|
|
47
48
|
const lines = [];
|
|
48
49
|
lines.push(`PR #${result.prNumber} merge-readiness check (via=${result.via})`);
|
|
49
50
|
lines.push(` HEAD SHA: ${result.headSha ?? "<unknown>"}`);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`
|
|
55
|
-
|
|
51
|
+
if (result.verdict.excludedAuthor) {
|
|
52
|
+
lines.push(" Greptile review: skipped (author excluded)");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(` Greptile reviewed: ${result.verdict.lastReviewedSha ?? "<not parsed>"}`);
|
|
56
|
+
const confidenceStr = result.verdict.confidence !== null ? String(result.verdict.confidence) : "<not parsed>";
|
|
57
|
+
lines.push(` Confidence: ${confidenceStr}/5`);
|
|
58
|
+
lines.push(` Findings: P0=${result.verdict.p0Count} ` +
|
|
59
|
+
`P1=${result.verdict.p1Count} P2=${result.verdict.p2Count}`);
|
|
60
|
+
lines.push(` Errored sentinel: ${result.verdict.errored ? "True" : "False"}`);
|
|
61
|
+
}
|
|
56
62
|
const ciBlock = result.partialData.ci;
|
|
57
63
|
if (ciBlock !== null && typeof ciBlock === "object" && !Array.isArray(ciBlock)) {
|
|
58
64
|
const ci = ciBlock;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GreptileVerdict } from "./types.js";
|
|
2
2
|
export declare function emptyVerdict(): GreptileVerdict;
|
|
3
|
+
export declare function isGreptileExcludedAuthor(body: string): boolean;
|
|
3
4
|
export declare function isInformalCleanMissingCanonicalFields(verdict: GreptileVerdict, body: string): boolean;
|
|
4
5
|
/** Parse a Greptile rolling-summary comment body into a structured verdict. */
|
|
5
6
|
export declare function parseGreptileBody(body: string): GreptileVerdict;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findLastReviewedCommitSha } from "../text/redos-safe.js";
|
|
2
|
-
import { CONFIDENCE_RE, GREPTILE_ERRORED_SENTINEL, INFORMAL_CLEAN_SIGNAL_RE, P0_BADGE, P1_BADGE, SECTION_RE, } from "./constants.js";
|
|
2
|
+
import { CONFIDENCE_RE, GREPTILE_ERRORED_SENTINEL, GREPTILE_EXCLUDED_AUTHOR_PHRASE, GREPTILE_STATUS_MARKER, INFORMAL_CLEAN_SIGNAL_RE, P0_BADGE, P1_BADGE, SECTION_RE, } from "./constants.js";
|
|
3
3
|
export function emptyVerdict() {
|
|
4
4
|
return {
|
|
5
5
|
found: false,
|
|
@@ -10,9 +10,15 @@ export function emptyVerdict() {
|
|
|
10
10
|
p1Count: 0,
|
|
11
11
|
p2Count: 0,
|
|
12
12
|
informalClean: false,
|
|
13
|
+
excludedAuthor: false,
|
|
13
14
|
rawBodyExcerpt: "",
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
export function isGreptileExcludedAuthor(body) {
|
|
18
|
+
const lower = body.toLowerCase();
|
|
19
|
+
return (lower.includes(GREPTILE_EXCLUDED_AUTHOR_PHRASE) &&
|
|
20
|
+
(lower.includes("greptile-status") || body.includes(GREPTILE_STATUS_MARKER)));
|
|
21
|
+
}
|
|
16
22
|
export function isInformalCleanMissingCanonicalFields(verdict, body) {
|
|
17
23
|
if (!verdict.found || verdict.errored) {
|
|
18
24
|
return false;
|
|
@@ -42,6 +48,7 @@ export function parseGreptileBody(body) {
|
|
|
42
48
|
return emptyVerdict();
|
|
43
49
|
}
|
|
44
50
|
const errored = body.trim().startsWith(GREPTILE_ERRORED_SENTINEL);
|
|
51
|
+
const excludedAuthor = isGreptileExcludedAuthor(body);
|
|
45
52
|
const lastReviewedSha = findLastReviewedCommitSha(body);
|
|
46
53
|
const confMatch = CONFIDENCE_RE.exec(body);
|
|
47
54
|
const confidence = confMatch?.groups?.score !== undefined ? Number(confMatch.groups.score) : null;
|
|
@@ -73,6 +80,7 @@ export function parseGreptileBody(body) {
|
|
|
73
80
|
p1Count,
|
|
74
81
|
p2Count,
|
|
75
82
|
informalClean: false,
|
|
83
|
+
excludedAuthor,
|
|
76
84
|
rawBodyExcerpt: body.slice(0, 200),
|
|
77
85
|
};
|
|
78
86
|
if (isInformalCleanMissingCanonicalFields(verdict, body)) {
|
|
@@ -7,6 +7,8 @@ export interface GreptileVerdict {
|
|
|
7
7
|
readonly p1Count: number;
|
|
8
8
|
readonly p2Count: number;
|
|
9
9
|
readonly informalClean: boolean;
|
|
10
|
+
/** Greptile deliberately skipped review for an excluded PR author (#2375). */
|
|
11
|
+
readonly excludedAuthor: boolean;
|
|
10
12
|
readonly rawBodyExcerpt: string;
|
|
11
13
|
}
|
|
12
14
|
export interface GateResult {
|
|
@@ -17,6 +17,8 @@ export declare const CANDIDATES_RELPATH: string;
|
|
|
17
17
|
export declare const DEFAULT_MAX_AGE_HOURS = 24;
|
|
18
18
|
export declare const ENV_MAX_AGE_HOURS = "DEFT_CACHE_MAX_AGE_HOURS";
|
|
19
19
|
export declare const ENV_TRIAGE_REPO = "DEFT_TRIAGE_REPO";
|
|
20
|
+
/** Set by releaseSubprocessEnv() during task release Step 5 (#2386). */
|
|
21
|
+
export declare const ENV_RELEASE_PREFLIGHT = "DEFT_RELEASE_PREFLIGHT";
|
|
20
22
|
export declare const REQUIRED_DECISION = "accept";
|
|
21
23
|
export interface GateResult {
|
|
22
24
|
readonly code: number;
|