@deftai/directive-core 0.66.1 → 0.67.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-advisory/evaluate.d.ts +44 -0
- package/dist/agents-md-advisory/evaluate.js +115 -0
- package/dist/agents-md-advisory/index.d.ts +2 -0
- package/dist/agents-md-advisory/index.js +2 -0
- package/dist/agents-md-budget/evaluate.d.ts +38 -0
- package/dist/agents-md-budget/evaluate.js +152 -0
- package/dist/agents-md-budget/index.d.ts +2 -0
- package/dist/agents-md-budget/index.js +2 -0
- package/dist/codebase/map.d.ts +1 -0
- package/dist/codebase/map.js +5 -2
- package/dist/doctor/constants.d.ts +5 -2
- package/dist/doctor/constants.js +15 -1
- package/dist/doctor/flags.js +5 -1
- package/dist/doctor/main.js +99 -8
- package/dist/doctor/paths.d.ts +1 -0
- package/dist/doctor/paths.js +55 -0
- package/dist/doctor/types.d.ts +7 -0
- package/dist/intake/github-auth-modes.d.ts +1 -0
- package/dist/intake/github-auth-modes.js +1 -0
- package/dist/intake/issue-ingest.d.ts +6 -2
- package/dist/intake/issue-ingest.js +65 -9
- package/dist/platform/agents-consumer-header.d.ts +13 -0
- package/dist/platform/agents-consumer-header.js +57 -0
- package/dist/platform/agents-md.js +4 -1
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/agents-md-advisory.d.ts +52 -0
- package/dist/policy/agents-md-advisory.js +63 -0
- package/dist/policy/agents-md-budget.d.ts +24 -0
- package/dist/policy/agents-md-budget.js +89 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
- package/dist/pr-merge-readiness/ci-gate.js +79 -0
- package/dist/pr-merge-readiness/compute.d.ts +5 -1
- package/dist/pr-merge-readiness/compute.js +90 -6
- package/dist/pr-merge-readiness/gh.d.ts +8 -0
- package/dist/pr-merge-readiness/gh.js +30 -5
- package/dist/pr-merge-readiness/index.d.ts +3 -0
- package/dist/pr-merge-readiness/index.js +2 -0
- package/dist/pr-merge-readiness/main.d.ts +3 -0
- package/dist/pr-merge-readiness/main.js +73 -6
- package/dist/pr-merge-readiness/output.js +20 -0
- package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
- package/dist/pr-merge-readiness/slizard-gate.js +119 -0
- package/dist/preflight-cache/evaluate.d.ts +1 -0
- package/dist/preflight-cache/evaluate.js +17 -10
- package/dist/render/project-render.d.ts +16 -2
- package/dist/render/project-render.js +55 -22
- package/dist/swarm/launch.d.ts +6 -0
- package/dist/swarm/launch.js +2 -2
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +11 -10
- package/dist/swarm/routing.d.ts +9 -0
- package/dist/swarm/routing.js +46 -0
- package/dist/swarm/verify-review-clean-cli.js +25 -1
- package/dist/swarm/verify-review-clean.d.ts +9 -1
- package/dist/swarm/verify-review-clean.js +76 -3
- package/dist/verify-source/biome-config.d.ts +41 -0
- package/dist/verify-source/biome-config.js +97 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/xbrief-migrate/agents-header.d.ts +69 -0
- package/dist/xbrief-migrate/agents-header.js +179 -0
- package/dist/xbrief-migrate/drift-gate.js +2 -0
- package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
- package/dist/xbrief-migrate/fs-helpers.js +11 -0
- package/dist/xbrief-migrate/index.d.ts +1 -0
- package/dist/xbrief-migrate/index.js +1 -0
- package/dist/xbrief-migrate/migrate-project.js +29 -10
- package/package.json +11 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated gate for the SLizard second-reviewer verdict (#2189).
|
|
3
|
+
*
|
|
4
|
+
* SLizard posts a check-run whose `output.summary` carries a structured verdict:
|
|
5
|
+
*
|
|
6
|
+
* Decision: request_changes
|
|
7
|
+
* Merge impact: blocking
|
|
8
|
+
* Findings: 2 (P0: 0, P1: 1, P2: 0, P3: 0)
|
|
9
|
+
*
|
|
10
|
+
* The generic CI check-run gate (#2169) only fails closed on a check-run
|
|
11
|
+
* `conclusion` in the failed set, so a blocking *decision* carried on a
|
|
12
|
+
* non-`failure` conclusion (e.g. `neutral`) would slip through and the review
|
|
13
|
+
* is surfaced indistinctly among build/test checks. This gate parses the
|
|
14
|
+
* structured verdict and fails merge-readiness on a blocking decision.
|
|
15
|
+
*/
|
|
16
|
+
/** Canonical SLizard check-run name; matching is case-insensitive substring for resilience. */
|
|
17
|
+
export const SLIZARD_CHECK_NAME = "SLizard";
|
|
18
|
+
const FAILED_CONCLUSIONS = new Set(["failure", "cancelled", "timed_out"]);
|
|
19
|
+
const PENDING_STATUSES = new Set(["queued", "in_progress"]);
|
|
20
|
+
const BLOCKING_DECISIONS = new Set(["request_changes", "changes_requested", "reject"]);
|
|
21
|
+
export function isSlizardCheck(name) {
|
|
22
|
+
return name.toLowerCase().includes("slizard");
|
|
23
|
+
}
|
|
24
|
+
function firstMatch(text, re) {
|
|
25
|
+
const m = re.exec(text);
|
|
26
|
+
return m?.[1] !== undefined ? m[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
function countFor(text, sev) {
|
|
29
|
+
const m = new RegExp(`${sev}\\s*:\\s*(\\d+)`, "i").exec(text);
|
|
30
|
+
return m?.[1] !== undefined ? Number.parseInt(m[1], 10) : null;
|
|
31
|
+
}
|
|
32
|
+
/** Parse a SLizard check-run `output.summary` into a structured verdict. */
|
|
33
|
+
export function parseSlizardVerdict(summary) {
|
|
34
|
+
const text = summary ?? "";
|
|
35
|
+
const decision = firstMatch(text, /Decision\s*:\s*([A-Za-z_]+)/i);
|
|
36
|
+
const mergeImpact = firstMatch(text, /Merge impact\s*:\s*([A-Za-z_-]+)/i);
|
|
37
|
+
return {
|
|
38
|
+
decision: decision ? decision.toLowerCase() : null,
|
|
39
|
+
mergeImpact: mergeImpact ? mergeImpact.toLowerCase() : null,
|
|
40
|
+
p0Count: countFor(text, "P0"),
|
|
41
|
+
p1Count: countFor(text, "P1"),
|
|
42
|
+
p2Count: countFor(text, "P2"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function skippedSummary(reason) {
|
|
46
|
+
return {
|
|
47
|
+
ready_state: "skipped",
|
|
48
|
+
present: false,
|
|
49
|
+
check_name: null,
|
|
50
|
+
status: null,
|
|
51
|
+
conclusion: null,
|
|
52
|
+
verdict: null,
|
|
53
|
+
summary_line: `SLizard review: skipped (${reason})`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isPending(status, conclusion) {
|
|
57
|
+
if (PENDING_STATUSES.has(status)) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return status !== "completed" || conclusion === "none";
|
|
61
|
+
}
|
|
62
|
+
export function evaluateSlizardGate(checkRuns, options = {}) {
|
|
63
|
+
if (options.skipSlizard === true) {
|
|
64
|
+
return { failures: [], summary: skippedSummary("--skip-slizard") };
|
|
65
|
+
}
|
|
66
|
+
const run = checkRuns.find((r) => isSlizardCheck(r.name));
|
|
67
|
+
if (run === undefined) {
|
|
68
|
+
// SLizard is an optional second reviewer; its absence does not block merge.
|
|
69
|
+
return { failures: [], summary: skippedSummary("no SLizard check-run on this commit") };
|
|
70
|
+
}
|
|
71
|
+
const verdict = parseSlizardVerdict(run.summary);
|
|
72
|
+
const blockingDecision = verdict.decision !== null && BLOCKING_DECISIONS.has(verdict.decision);
|
|
73
|
+
const blockingImpact = verdict.mergeImpact === "blocking";
|
|
74
|
+
const failedConclusion = FAILED_CONCLUSIONS.has(run.conclusion);
|
|
75
|
+
const failures = [];
|
|
76
|
+
let readyState;
|
|
77
|
+
if (blockingDecision || blockingImpact || failedConclusion) {
|
|
78
|
+
readyState = "blocked";
|
|
79
|
+
const reasons = [];
|
|
80
|
+
if (verdict.decision !== null) {
|
|
81
|
+
reasons.push(`decision=${verdict.decision}`);
|
|
82
|
+
}
|
|
83
|
+
if (verdict.mergeImpact !== null) {
|
|
84
|
+
reasons.push(`merge impact=${verdict.mergeImpact}`);
|
|
85
|
+
}
|
|
86
|
+
if (failedConclusion) {
|
|
87
|
+
reasons.push(`conclusion=${run.conclusion}`);
|
|
88
|
+
}
|
|
89
|
+
const findings = verdict.p0Count !== null || verdict.p1Count !== null || verdict.p2Count !== null
|
|
90
|
+
? ` (P0=${verdict.p0Count ?? 0} P1=${verdict.p1Count ?? 0} P2=${verdict.p2Count ?? 0})`
|
|
91
|
+
: "";
|
|
92
|
+
failures.push(`SLizard review is blocking: ${reasons.join(", ")}${findings}. ` +
|
|
93
|
+
"Resolve the SLizard findings or pass --skip-slizard to override (#2189).");
|
|
94
|
+
}
|
|
95
|
+
else if (isPending(run.status, run.conclusion)) {
|
|
96
|
+
readyState = "not_ready_yet";
|
|
97
|
+
failures.push(`SLizard review still in progress (${run.status}); wait for the verdict before merge (#2189).`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
readyState = "ready";
|
|
101
|
+
}
|
|
102
|
+
const parts = [
|
|
103
|
+
`decision=${verdict.decision ?? "?"}`,
|
|
104
|
+
`impact=${verdict.mergeImpact ?? "?"}`,
|
|
105
|
+
];
|
|
106
|
+
return {
|
|
107
|
+
failures,
|
|
108
|
+
summary: {
|
|
109
|
+
ready_state: readyState,
|
|
110
|
+
present: true,
|
|
111
|
+
check_name: run.name,
|
|
112
|
+
status: run.status,
|
|
113
|
+
conclusion: run.conclusion,
|
|
114
|
+
verdict,
|
|
115
|
+
summary_line: `SLizard review: ${readyState} (${parts.join(", ")})`,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=slizard-gate.js.map
|
|
@@ -34,6 +34,7 @@ export interface EvaluateOptions {
|
|
|
34
34
|
/** Injectable drift probe for tests. */
|
|
35
35
|
probeDriftFn?: (repo: string, cacheRoot: string, source: string) => CacheDriftProbeResult | null;
|
|
36
36
|
}
|
|
37
|
+
export declare function normaliseRepoUrl(url: string): string | null;
|
|
37
38
|
/**
|
|
38
39
|
* Branch-aware recovery hint (#1953 Option 3).
|
|
39
40
|
* Age-stale (or age+drift mixed) → --force bypasses TTL; drift-only → plain refetch.
|
|
@@ -44,19 +44,26 @@ function inferRepoFromGit(projectRoot) {
|
|
|
44
44
|
return null;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
function normaliseRepoUrl(url) {
|
|
47
|
+
export function normaliseRepoUrl(url) {
|
|
48
48
|
if (!url)
|
|
49
49
|
return null;
|
|
50
|
-
const cleaned = url
|
|
51
|
-
|
|
50
|
+
const cleaned = url
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/\/$/, "")
|
|
53
|
+
.replace(/\.git$/, "");
|
|
54
|
+
// Match `github.com` only as the HOST component of the remote URL: right
|
|
55
|
+
// after an optional scheme and optional `user@`, and immediately followed by
|
|
56
|
+
// `:` or `/`. A plain `includes("github.com")` would also accept spoofed
|
|
57
|
+
// hosts like `github.com.evil.com` or `evil.com/github.com/...` (CodeQL
|
|
58
|
+
// js/incomplete-url-substring-sanitization). Supports https/ssh/git schemes
|
|
59
|
+
// and the scp-like `git@github.com:owner/repo` form.
|
|
60
|
+
const match = cleaned.match(/^(?:(?:https?|ssh|git):\/\/)?(?:[^/@]+@)?github\.com[:/]+([^/]+)\/([^/]+)/i);
|
|
61
|
+
if (!match)
|
|
52
62
|
return null;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const parts = tail.split("/");
|
|
58
|
-
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
59
|
-
return `${parts[0]}/${parts[1]}`;
|
|
63
|
+
const owner = match[1];
|
|
64
|
+
const repo = match[2];
|
|
65
|
+
if (owner && repo) {
|
|
66
|
+
return `${owner}/${repo}`;
|
|
60
67
|
}
|
|
61
68
|
return null;
|
|
62
69
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { LEGACY_INFO_ROOT_KEY, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
|
|
1
2
|
type JsonObject = Record<string, unknown>;
|
|
3
|
+
/** PROJECT-DEFINITION artifact shape (filename + envelope key + emitted version). */
|
|
4
|
+
interface ProjectDefinitionLayout {
|
|
5
|
+
readonly filename: string;
|
|
6
|
+
readonly infoRootKey: typeof MIGRATED_INFO_ROOT_KEY | typeof LEGACY_INFO_ROOT_KEY;
|
|
7
|
+
readonly infoVersion: string;
|
|
8
|
+
}
|
|
2
9
|
/** Durable review state for PROJECT-DEFINITION narrative staleness (#640). */
|
|
3
10
|
export interface StalenessReviewMetadata {
|
|
4
11
|
/** ISO-8601 UTC when narratives were last reviewed/acknowledged. */
|
|
@@ -24,12 +31,19 @@ export declare function buildStalenessAcknowledgement(completedItems: readonly L
|
|
|
24
31
|
existing?: StalenessReviewMetadata | null;
|
|
25
32
|
}): StalenessReviewMetadata;
|
|
26
33
|
export declare function computeStalenessFlags(narratives: Record<string, string>, completedItems: readonly LifecycleItem[], review?: StalenessReviewMetadata | null): string[];
|
|
27
|
-
export declare function createSkeleton(items: LifecycleItem[], now: string): JsonObject;
|
|
34
|
+
export declare function createSkeleton(items: LifecycleItem[], now: string, layout: ProjectDefinitionLayout): JsonObject;
|
|
28
35
|
export interface RenderProjectOptions {
|
|
29
36
|
readonly now?: Date;
|
|
30
37
|
}
|
|
31
38
|
export type RenderProjectResult = readonly [boolean, string];
|
|
32
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Regenerate the PROJECT-DEFINITION artifact for `vbriefDir`.
|
|
41
|
+
*
|
|
42
|
+
* Layout-aware (#2149): on a migrated `xbrief/` root it targets
|
|
43
|
+
* `PROJECT-DEFINITION.xbrief.json` with an `xBRIEFInfo` envelope; on a legacy `vbrief/`
|
|
44
|
+
* root it keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
|
|
45
|
+
* (Mirrors ``scripts/project_render.render_project_definition``.)
|
|
46
|
+
*/
|
|
33
47
|
export declare function renderProjectDefinition(vbriefDir: string, options?: RenderProjectOptions): RenderProjectResult;
|
|
34
48
|
/**
|
|
35
49
|
* Mark current completed scopes as reviewed for PROJECT-DEFINITION narratives.
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
|
|
4
4
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
5
5
|
import { deriveRegistryItemStatus, registryMetadataReferencesFromScope, } from "../vbrief-validate/registry-status.js";
|
|
6
|
+
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
|
|
6
7
|
import { PROJECT_LIFECYCLE_FOLDERS, SKELETON_NARRATIVES } from "./constants.js";
|
|
7
8
|
import { splitCamel, splitWords } from "./text-utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the PROJECT-DEFINITION artifact shape for a lifecycle root directory.
|
|
11
|
+
*
|
|
12
|
+
* The decision is STRUCTURAL and keyed on the lifecycle root directory name -- the
|
|
13
|
+
* same signal `resolveLifecycleLayout` / `resolveLifecycleRoot` produce, so ingest and
|
|
14
|
+
* render never diverge (#2149). A migrated `xbrief/` root gets `PROJECT-DEFINITION.xbrief.json`
|
|
15
|
+
* + `xBRIEFInfo`; a legacy `vbrief/` root keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
|
|
16
|
+
* This prevents render from writing a legacy-named/enveloped artifact into a migrated tree.
|
|
17
|
+
*/
|
|
18
|
+
function resolveProjectDefinitionLayout(vbriefDir) {
|
|
19
|
+
const migrated = basename(vbriefDir) === MIGRATED_ARTIFACT_DIR;
|
|
20
|
+
return migrated
|
|
21
|
+
? {
|
|
22
|
+
filename: `PROJECT-DEFINITION${MIGRATED_ARTIFACT_SUFFIX}`,
|
|
23
|
+
infoRootKey: MIGRATED_INFO_ROOT_KEY,
|
|
24
|
+
infoVersion: VBRIEF_VERSION,
|
|
25
|
+
}
|
|
26
|
+
: {
|
|
27
|
+
filename: `PROJECT-DEFINITION${LEGACY_ARTIFACT_SUFFIX}`,
|
|
28
|
+
infoRootKey: LEGACY_INFO_ROOT_KEY,
|
|
29
|
+
infoVersion: EMITTED_VBRIEF_VERSION,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
8
32
|
export function scanLifecycleFolders(vbriefDir) {
|
|
9
33
|
const items = [];
|
|
10
34
|
for (const folderName of PROJECT_LIFECYCLE_FOLDERS) {
|
|
@@ -25,11 +49,11 @@ export function scanLifecycleFolders(vbriefDir) {
|
|
|
25
49
|
try {
|
|
26
50
|
const data = JSON.parse(readFileSync(full, "utf8"));
|
|
27
51
|
const plan = (data.plan ?? {});
|
|
28
|
-
const title = String(plan.title ?? vbriefFile
|
|
52
|
+
const title = String(plan.title ?? stripArtifactSuffix(vbriefFile));
|
|
29
53
|
const status = deriveRegistryItemStatus(plan.status, folderName);
|
|
30
54
|
const references = registryMetadataReferencesFromScope(plan.references);
|
|
31
55
|
const item = {
|
|
32
|
-
id: vbriefFile
|
|
56
|
+
id: stripArtifactSuffix(vbriefFile),
|
|
33
57
|
title,
|
|
34
58
|
status,
|
|
35
59
|
metadata: {
|
|
@@ -44,7 +68,7 @@ export function scanLifecycleFolders(vbriefDir) {
|
|
|
44
68
|
}
|
|
45
69
|
catch {
|
|
46
70
|
items.push({
|
|
47
|
-
id: vbriefFile
|
|
71
|
+
id: stripArtifactSuffix(vbriefFile),
|
|
48
72
|
title: `[unreadable] ${vbriefFile}`,
|
|
49
73
|
status: "draft",
|
|
50
74
|
metadata: {
|
|
@@ -136,12 +160,12 @@ export function computeStalenessFlags(narratives, completedItems, review = null)
|
|
|
136
160
|
const pending = unacknowledgedCompletedItems(completedItems, review);
|
|
137
161
|
return flagStaleNarratives(narratives, pending);
|
|
138
162
|
}
|
|
139
|
-
export function createSkeleton(items, now) {
|
|
163
|
+
export function createSkeleton(items, now, layout) {
|
|
140
164
|
const completedItems = items.filter((i) => i.status === "completed");
|
|
141
165
|
const stalenessFlags = computeStalenessFlags({ ...SKELETON_NARRATIVES }, completedItems);
|
|
142
166
|
return {
|
|
143
|
-
|
|
144
|
-
version:
|
|
167
|
+
[layout.infoRootKey]: {
|
|
168
|
+
version: layout.infoVersion,
|
|
145
169
|
description: "Project definition -- synthesized gestalt of the project",
|
|
146
170
|
created: now,
|
|
147
171
|
updated: now,
|
|
@@ -155,11 +179,19 @@ export function createSkeleton(items, now) {
|
|
|
155
179
|
},
|
|
156
180
|
};
|
|
157
181
|
}
|
|
158
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Regenerate the PROJECT-DEFINITION artifact for `vbriefDir`.
|
|
184
|
+
*
|
|
185
|
+
* Layout-aware (#2149): on a migrated `xbrief/` root it targets
|
|
186
|
+
* `PROJECT-DEFINITION.xbrief.json` with an `xBRIEFInfo` envelope; on a legacy `vbrief/`
|
|
187
|
+
* root it keeps `PROJECT-DEFINITION.vbrief.json` + `vBRIEFInfo`.
|
|
188
|
+
* (Mirrors ``scripts/project_render.render_project_definition``.)
|
|
189
|
+
*/
|
|
159
190
|
export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
160
191
|
const nowDate = options.now ?? new Date();
|
|
161
192
|
const now = nowDate.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
162
|
-
const
|
|
193
|
+
const layout = resolveProjectDefinitionLayout(vbriefDir);
|
|
194
|
+
const projectDefPath = join(vbriefDir, layout.filename);
|
|
163
195
|
const items = scanLifecycleFolders(vbriefDir);
|
|
164
196
|
const createdNew = !existsSync(projectDefPath);
|
|
165
197
|
let projectDef;
|
|
@@ -172,10 +204,11 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
172
204
|
}
|
|
173
205
|
const plan = (projectDef.plan ?? {});
|
|
174
206
|
plan.items = items;
|
|
175
|
-
if (typeof projectDef.
|
|
176
|
-
projectDef.
|
|
207
|
+
if (typeof projectDef[layout.infoRootKey] !== "object" ||
|
|
208
|
+
projectDef[layout.infoRootKey] === null) {
|
|
209
|
+
projectDef[layout.infoRootKey] = {};
|
|
177
210
|
}
|
|
178
|
-
projectDef.
|
|
211
|
+
projectDef[layout.infoRootKey].updated = now;
|
|
179
212
|
const narratives = typeof plan.narratives === "object" &&
|
|
180
213
|
plan.narratives !== null &&
|
|
181
214
|
!Array.isArray(plan.narratives)
|
|
@@ -194,7 +227,7 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
194
227
|
projectDef.plan = plan;
|
|
195
228
|
}
|
|
196
229
|
else {
|
|
197
|
-
projectDef = createSkeleton(items, now);
|
|
230
|
+
projectDef = createSkeleton(items, now, layout);
|
|
198
231
|
}
|
|
199
232
|
mkdirSync(vbriefDir, { recursive: true });
|
|
200
233
|
writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
|
|
@@ -202,7 +235,7 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
202
235
|
const planMeta = (projectDef.plan?.metadata ?? {});
|
|
203
236
|
const flagCount = Array.isArray(planMeta.staleness_flags) ? planMeta.staleness_flags.length : 0;
|
|
204
237
|
const action = createdNew ? "created" : "updated";
|
|
205
|
-
const parts = [`✓
|
|
238
|
+
const parts = [`✓ ${layout.filename} ${action} (${itemCount} scope items)`];
|
|
206
239
|
if (flagCount > 0)
|
|
207
240
|
parts.push(`⚠ ${flagCount} staleness flag(s) -- agent review recommended`);
|
|
208
241
|
return [true, parts.join("\n")];
|
|
@@ -216,7 +249,8 @@ export function renderProjectDefinition(vbriefDir, options = {}) {
|
|
|
216
249
|
export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
|
|
217
250
|
const nowDate = options.now ?? new Date();
|
|
218
251
|
const now = isoTimestamp(nowDate);
|
|
219
|
-
const
|
|
252
|
+
const layout = resolveProjectDefinitionLayout(vbriefDir);
|
|
253
|
+
const projectDefPath = join(vbriefDir, layout.filename);
|
|
220
254
|
if (!existsSync(projectDefPath)) {
|
|
221
255
|
return [false, `✗ ${projectDefPath} not found — run project:render first`];
|
|
222
256
|
}
|
|
@@ -245,10 +279,11 @@ export function acknowledgeProjectDefinitionStaleness(vbriefDir, options = {}) {
|
|
|
245
279
|
? plan.narratives
|
|
246
280
|
: {};
|
|
247
281
|
planMetadata.staleness_flags = computeStalenessFlags(narratives, completedItems, parseStalenessReview(planMetadata));
|
|
248
|
-
if (typeof projectDef.
|
|
249
|
-
projectDef.
|
|
282
|
+
if (typeof projectDef[layout.infoRootKey] !== "object" ||
|
|
283
|
+
projectDef[layout.infoRootKey] === null) {
|
|
284
|
+
projectDef[layout.infoRootKey] = {};
|
|
250
285
|
}
|
|
251
|
-
projectDef.
|
|
286
|
+
projectDef[layout.infoRootKey].updated = now;
|
|
252
287
|
projectDef.plan = plan;
|
|
253
288
|
writeFileSync(projectDefPath, `${JSON.stringify(projectDef, null, 2)}\n`, "utf8");
|
|
254
289
|
const ackCount = completedItems.length;
|
|
@@ -280,9 +315,7 @@ export function main(argv) {
|
|
|
280
315
|
process.stderr.write("Usage: project-render [--acknowledge-staleness] [--project-root <dir>] [vbrief_dir]\n");
|
|
281
316
|
return 2;
|
|
282
317
|
}
|
|
283
|
-
const vbriefDir = projectRoot !== undefined
|
|
284
|
-
? resolveLifecycleRoot(resolve(projectRoot))
|
|
285
|
-
: (positional[0] ?? "vbrief");
|
|
318
|
+
const vbriefDir = positional[0] ?? resolveLifecycleRoot(resolve(projectRoot !== undefined ? projectRoot : "."));
|
|
286
319
|
const [ok, message] = acknowledge
|
|
287
320
|
? acknowledgeProjectDefinitionStaleness(vbriefDir)
|
|
288
321
|
: renderProjectDefinition(vbriefDir);
|
package/dist/swarm/launch.d.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface LaunchArgs {
|
|
|
68
68
|
readinessGate?: ReadinessGateFn;
|
|
69
69
|
worktreeResolver?: WorktreeResolverFn;
|
|
70
70
|
runtimeAuthProbe?: RuntimeAuthProbeFn;
|
|
71
|
+
/**
|
|
72
|
+
* Injection seam for the routing-provider environment lookup, mirroring
|
|
73
|
+
* `resolveRoutingPath`'s `environ` parameter (#1877 Greptile follow-up).
|
|
74
|
+
* Defaults to `process.env` when unset.
|
|
75
|
+
*/
|
|
76
|
+
environ?: NodeJS.ProcessEnv;
|
|
71
77
|
}
|
|
72
78
|
export declare function swarmLaunch(args: LaunchArgs): {
|
|
73
79
|
exitCode: number;
|
package/dist/swarm/launch.js
CHANGED
|
@@ -8,7 +8,7 @@ import { issueNumbersFromPlan, scopeMetadataRank } from "../triage/queue/scope-w
|
|
|
8
8
|
import { selectionOrderingKey } from "../triage/queue/selection.js";
|
|
9
9
|
import { DEFAULT_BASE_BRANCH, EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK, GATE_ADVISE, GATE_ENFORCE, LEAF_CODING_WORKER_ROLE, } from "./constants.js";
|
|
10
10
|
import { readinessReport } from "./readiness.js";
|
|
11
|
-
import {
|
|
11
|
+
import { loadRoutingFile, resolveDispatchProvider, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
|
|
12
12
|
import { dispatchProviderFor, enforceSubagentBackendPolicy } from "./subagent-backend.js";
|
|
13
13
|
import { resolveWorktreeMap } from "./worktrees.js";
|
|
14
14
|
export const defaultPreflightGate = (vbriefPath) => {
|
|
@@ -525,7 +525,7 @@ export function swarmLaunch(args) {
|
|
|
525
525
|
let modelSource = null;
|
|
526
526
|
let routingProvider = null;
|
|
527
527
|
if (routingFile !== null) {
|
|
528
|
-
routingProvider =
|
|
528
|
+
routingProvider = resolveDispatchProvider(args.environ ?? process.env);
|
|
529
529
|
const route = resolveModelRoute(routingFile, routingProvider, LEAF_CODING_WORKER_ROLE);
|
|
530
530
|
// A malformed decision object must fail loud here: the legacy backend gate
|
|
531
531
|
// was already bypassed above (routingFile !== null), so silently continuing
|
|
@@ -9,7 +9,7 @@ export interface VerifyRoutingOptions {
|
|
|
9
9
|
advise?: boolean;
|
|
10
10
|
/** Override the resolved provider (else derived from the runtime). */
|
|
11
11
|
provider?: string | null;
|
|
12
|
-
/** Inject the runtime descriptor (else
|
|
12
|
+
/** Inject the runtime descriptor (legacy test seam; else resolveDispatchProvider). */
|
|
13
13
|
runtimeProbe?: () => string;
|
|
14
14
|
}
|
|
15
15
|
export interface VerifyRoutingResult {
|
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* 1 = at least one in-scope role is undecided / not dispatchable
|
|
9
9
|
* 2 = config error (unreadable / malformed route file)
|
|
10
10
|
*/
|
|
11
|
-
import { getPlatformCapabilities } from "../intake/platform-capabilities.js";
|
|
12
11
|
import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
|
|
13
|
-
import { dispatchProviderFromRuntime, HARNESS_BOUND_PROVIDERS, loadRoutingFile, ROUTING_MODE_HARNESS_DEFAULT, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
|
|
12
|
+
import { dispatchProviderFromRuntime, HARNESS_BOUND_PROVIDERS, loadRoutingFile, ROUTING_MODE_HARNESS_DEFAULT, resolveDispatchProvider, resolveModelRoute, resolveRoutingPath, } from "./routing.js";
|
|
14
13
|
export const ROUTING_SET_CMD = "task swarm:routing-set -- --role <role> --model <slug> (or --harness-default)";
|
|
15
14
|
/** Roles the pre-dispatch gate checks by default: the actual model lever. */
|
|
16
15
|
export const DEFAULT_GATED_ROLES = ["leaf-implementation"];
|
|
@@ -18,15 +17,17 @@ function resolveProvider(options) {
|
|
|
18
17
|
if (options.provider !== undefined && options.provider !== null && options.provider.length > 0) {
|
|
19
18
|
return options.provider;
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
if (options.runtimeProbe !== undefined) {
|
|
21
|
+
let runtimeMode = "";
|
|
22
|
+
try {
|
|
23
|
+
runtimeMode = options.runtimeProbe();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
runtimeMode = "";
|
|
27
|
+
}
|
|
28
|
+
return dispatchProviderFromRuntime(runtimeMode);
|
|
28
29
|
}
|
|
29
|
-
return
|
|
30
|
+
return resolveDispatchProvider(options.environ ?? process.env);
|
|
30
31
|
}
|
|
31
32
|
export function verifyRouting(options) {
|
|
32
33
|
const roles = options.roles && options.roles.length > 0 ? options.roles : DEFAULT_GATED_ROLES;
|
package/dist/swarm/routing.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
|
|
|
9
9
|
export declare const ROUTING_FILENAME = "routing.local.json";
|
|
10
10
|
/** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
|
|
11
11
|
export declare const HARNESS_BOUND_PROVIDERS: Set<string>;
|
|
12
|
+
/** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
|
|
13
|
+
export declare const ROUTING_GATED_DISPATCH_PROVIDERS: Set<string>;
|
|
12
14
|
export interface RouteDecision {
|
|
13
15
|
model: string | null;
|
|
14
16
|
mode?: string;
|
|
@@ -48,6 +50,13 @@ export declare function loadRoutingFile(path: string): {
|
|
|
48
50
|
export declare function resolveModelRoute(file: RoutingFile | null, provider: string, role: string): RouteResolution;
|
|
49
51
|
/** Map a runtime descriptor (platform-capabilities.runtimeMode) to a route key. */
|
|
50
52
|
export declare function dispatchProviderFromRuntime(runtimeMode: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the `dispatch_provider` routing key from the active runtime envelope.
|
|
55
|
+
* Separate from `runtime_mode` (#1557): Cursor sessions may carry
|
|
56
|
+
* `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
|
|
57
|
+
* `cursor` for model selection (#1877).
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveDispatchProvider(environ?: NodeJS.ProcessEnv): string;
|
|
51
60
|
/**
|
|
52
61
|
* Write a decision back to the route file (create-if-missing). Stamps
|
|
53
62
|
* `decidedAt` when the caller did not supply one. Used by the interactive
|
package/dist/swarm/routing.js
CHANGED
|
@@ -31,6 +31,12 @@ export const ROUTING_MODE_HARNESS_DEFAULT = "harness-default";
|
|
|
31
31
|
export const ROUTING_FILENAME = "routing.local.json";
|
|
32
32
|
/** Providers whose model is harness-bound -- deft cannot pin or verify a slug. */
|
|
33
33
|
export const HARNESS_BOUND_PROVIDERS = new Set(["grok"]);
|
|
34
|
+
/** Providers whose per-role model must be decided before sub-agent dispatch (#1739 / #1877). */
|
|
35
|
+
export const ROUTING_GATED_DISPATCH_PROVIDERS = new Set(["cursor", "grok"]);
|
|
36
|
+
const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
|
|
37
|
+
function envTruthy(environ, name) {
|
|
38
|
+
return TRUTHY_ENV.has((environ[name] ?? "").trim().toLowerCase());
|
|
39
|
+
}
|
|
34
40
|
/**
|
|
35
41
|
* Resolve the route-file path. Honors the `DEFT_ROUTING_PATH` override first
|
|
36
42
|
* (keeps both maintainer and consumer testable), then reads from the MAIN
|
|
@@ -150,12 +156,52 @@ export function dispatchProviderFromRuntime(runtimeMode) {
|
|
|
150
156
|
}
|
|
151
157
|
return normalized;
|
|
152
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the `dispatch_provider` routing key from the active runtime envelope.
|
|
161
|
+
* Separate from `runtime_mode` (#1557): Cursor sessions may carry
|
|
162
|
+
* `runtime_mode=cloud-headless` for gh-auth purposes but route under provider
|
|
163
|
+
* `cursor` for model selection (#1877).
|
|
164
|
+
*/
|
|
165
|
+
export function resolveDispatchProvider(environ = process.env) {
|
|
166
|
+
if (envTruthy(environ, "CURSOR_COMPOSER") || envTruthy(environ, "CURSOR_AGENT")) {
|
|
167
|
+
return "cursor";
|
|
168
|
+
}
|
|
169
|
+
const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
|
|
170
|
+
if (envTruthy(environ, "GROK_BUILD") || runtime === "grok-build") {
|
|
171
|
+
return "grok";
|
|
172
|
+
}
|
|
173
|
+
if (runtime === "cloud" || runtime === "headless") {
|
|
174
|
+
return "cloud-headless";
|
|
175
|
+
}
|
|
176
|
+
if (envTruthy(environ, "GITHUB_ACTIONS") ||
|
|
177
|
+
envTruthy(environ, "BUILDKITE") ||
|
|
178
|
+
(envTruthy(environ, "CI") &&
|
|
179
|
+
!envTruthy(environ, "CURSOR_COMPOSER") &&
|
|
180
|
+
!envTruthy(environ, "CURSOR_AGENT"))) {
|
|
181
|
+
return "cloud-headless";
|
|
182
|
+
}
|
|
183
|
+
return "unknown";
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Keys that would mutate the prototype chain rather than set an own property
|
|
187
|
+
* if used as a computed object key. Rejected for provider/role names so a
|
|
188
|
+
* malicious routing input cannot pollute `Object.prototype` (CodeQL
|
|
189
|
+
* js/prototype-polluting-assignment).
|
|
190
|
+
*/
|
|
191
|
+
const FORBIDDEN_ROUTING_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
192
|
+
function assertSafeRoutingKey(kind, key) {
|
|
193
|
+
if (FORBIDDEN_ROUTING_KEYS.has(key)) {
|
|
194
|
+
throw new Error(`routing ${kind} name "${key}" is not allowed`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
153
197
|
/**
|
|
154
198
|
* Write a decision back to the route file (create-if-missing). Stamps
|
|
155
199
|
* `decidedAt` when the caller did not supply one. Used by the interactive
|
|
156
200
|
* resolver path (resolver step 5) and the `swarm:routing-set` task.
|
|
157
201
|
*/
|
|
158
202
|
export function writeModelDecision(path, provider, role, decision) {
|
|
203
|
+
assertSafeRoutingKey("provider", provider);
|
|
204
|
+
assertSafeRoutingKey("role", role);
|
|
159
205
|
const { data } = loadRoutingFile(path);
|
|
160
206
|
const file = data ?? {};
|
|
161
207
|
const existing = providerBlockOf(file, provider);
|
|
@@ -4,8 +4,11 @@ import { verifyReviewClean } from "./verify-review-clean.js";
|
|
|
4
4
|
export function verifyReviewCleanMain(argv = process.argv.slice(2)) {
|
|
5
5
|
const prNumbers = [];
|
|
6
6
|
const cohortGlobs = [];
|
|
7
|
+
const ciIgnoreChecks = [];
|
|
7
8
|
let repo = null;
|
|
8
9
|
let emitJson = false;
|
|
10
|
+
let skipCi = false;
|
|
11
|
+
let skipSlizard = false;
|
|
9
12
|
for (let i = 0; i < argv.length; i += 1) {
|
|
10
13
|
const arg = argv[i];
|
|
11
14
|
if (arg === "--cohort" && argv[i + 1] !== undefined) {
|
|
@@ -19,11 +22,32 @@ export function verifyReviewCleanMain(argv = process.argv.slice(2)) {
|
|
|
19
22
|
else if (arg === "--json") {
|
|
20
23
|
emitJson = true;
|
|
21
24
|
}
|
|
25
|
+
else if (arg === "--skip-ci") {
|
|
26
|
+
skipCi = true;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === "--skip-slizard") {
|
|
29
|
+
skipSlizard = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === "--ci-ignore-check" && argv[i + 1] !== undefined) {
|
|
32
|
+
ciIgnoreChecks.push(argv[i + 1] ?? "");
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
else if (arg?.startsWith("--ci-ignore-check=")) {
|
|
36
|
+
ciIgnoreChecks.push(arg.slice("--ci-ignore-check=".length));
|
|
37
|
+
}
|
|
22
38
|
else if (arg !== undefined && /^\d+$/.test(arg)) {
|
|
23
39
|
prNumbers.push(Number.parseInt(arg, 10));
|
|
24
40
|
}
|
|
25
41
|
}
|
|
26
|
-
const result = verifyReviewClean({
|
|
42
|
+
const result = verifyReviewClean({
|
|
43
|
+
prNumbers,
|
|
44
|
+
cohortGlobs,
|
|
45
|
+
repo,
|
|
46
|
+
emitJson,
|
|
47
|
+
skipCi,
|
|
48
|
+
skipSlizard,
|
|
49
|
+
ciIgnoreChecks,
|
|
50
|
+
});
|
|
27
51
|
if (result.stdout.length > 0) {
|
|
28
52
|
process.stdout.write(result.stdout);
|
|
29
53
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { CiGateOptions } from "../pr-merge-readiness/ci-gate.js";
|
|
2
|
+
import type { SlizardGateOptions } from "../pr-merge-readiness/slizard-gate.js";
|
|
1
3
|
import type { RunGhFn } from "../pr-merge-readiness/types.js";
|
|
4
|
+
type ReviewGateOptions = CiGateOptions & SlizardGateOptions;
|
|
2
5
|
import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
|
|
3
6
|
export interface CohortResolutionError {
|
|
4
7
|
vbrief_path: string;
|
|
@@ -10,6 +13,8 @@ export interface CohortPRResult {
|
|
|
10
13
|
verdict: Record<string, unknown>;
|
|
11
14
|
failures: string[];
|
|
12
15
|
clean: boolean;
|
|
16
|
+
ci_summary: Record<string, unknown> | null;
|
|
17
|
+
slizard_summary: Record<string, unknown> | null;
|
|
13
18
|
}
|
|
14
19
|
export interface CohortResult {
|
|
15
20
|
repo: string | null;
|
|
@@ -21,7 +26,7 @@ export declare function resolveCohortFromVbriefs(vbriefGlobs: readonly string[])
|
|
|
21
26
|
prNumbers: number[];
|
|
22
27
|
failures: CohortResolutionError[];
|
|
23
28
|
};
|
|
24
|
-
export declare function evaluatePr(prNumber: number, repo: string | null, runGh?: RunGhFn): CohortPRResult | null;
|
|
29
|
+
export declare function evaluatePr(prNumber: number, repo: string | null, runGh?: RunGhFn, options?: ReviewGateOptions): CohortPRResult | null;
|
|
25
30
|
export declare function cohortResultToDict(cohort: CohortResult): Record<string, unknown>;
|
|
26
31
|
export declare function renderReviewCleanText(cohort: CohortResult): string;
|
|
27
32
|
export interface VerifyReviewCleanArgs {
|
|
@@ -29,6 +34,9 @@ export interface VerifyReviewCleanArgs {
|
|
|
29
34
|
cohortGlobs?: readonly string[];
|
|
30
35
|
repo?: string | null;
|
|
31
36
|
emitJson?: boolean;
|
|
37
|
+
skipCi?: boolean;
|
|
38
|
+
skipSlizard?: boolean;
|
|
39
|
+
ciIgnoreChecks?: readonly string[];
|
|
32
40
|
runGh?: RunGhFn;
|
|
33
41
|
}
|
|
34
42
|
export declare function verifyReviewClean(args: VerifyReviewCleanArgs): {
|