@deftai/directive-core 0.78.0 → 0.79.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/cache/empty-populate.d.ts +44 -0
- package/dist/cache/empty-populate.js +117 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/check-updates/index.d.ts +10 -0
- package/dist/check-updates/index.js +81 -1
- package/dist/doctor/main.js +13 -2
- package/dist/doctor/payload-staleness.js +7 -1
- package/dist/hooks/dispatcher.d.ts +1 -1
- package/dist/hooks/dispatcher.js +19 -17
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +10 -0
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/init-deposit/init-deposit.js +2 -0
- package/dist/init-deposit/prettierignore.d.ts +21 -0
- package/dist/init-deposit/prettierignore.js +83 -0
- package/dist/init-deposit/refresh.d.ts +3 -1
- package/dist/init-deposit/refresh.js +29 -40
- package/dist/init-deposit/scaffold.js +6 -12
- package/dist/init-deposit/xbrief-projections.d.ts +18 -0
- package/dist/init-deposit/xbrief-projections.js +117 -0
- package/dist/intake/reconcile-issues.d.ts +10 -0
- package/dist/intake/reconcile-issues.js +115 -2
- package/dist/issue-sync/index.d.ts +2 -0
- package/dist/issue-sync/index.js +2 -0
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
- package/dist/issue-sync/sync-from-xbrief.js +208 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +5 -0
- package/dist/platform/platform-capabilities.js +9 -0
- package/dist/platform/shell-context.d.ts +30 -0
- package/dist/platform/shell-context.js +111 -0
- package/dist/pr-monitor/cadence.d.ts +6 -0
- package/dist/pr-monitor/cadence.js +13 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +7 -0
- package/dist/pr-monitor/monitor.js +59 -13
- package/dist/preflight-cache/evaluate.d.ts +3 -8
- package/dist/preflight-cache/evaluate.js +19 -19
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +8 -1
- package/dist/release/flags.js +26 -0
- package/dist/release/issue-state-fetch.d.ts +27 -0
- package/dist/release/issue-state-fetch.js +146 -0
- package/dist/release/main.js +1 -0
- package/dist/release/native-steps.js +6 -4
- package/dist/release/pipeline.js +15 -5
- package/dist/release/preflight.d.ts +6 -2
- package/dist/release/preflight.js +19 -5
- package/dist/release/types.d.ts +3 -1
- package/dist/release-e2e/constants.js +6 -6
- package/dist/release-e2e/flags.d.ts +2 -0
- package/dist/release-e2e/flags.js +8 -4
- package/dist/release-e2e/main.js +15 -9
- package/dist/release-e2e/types.d.ts +4 -2
- package/dist/resolution/classify.d.ts +1 -1
- package/dist/resolution/classify.js +36 -11
- package/dist/scope/project-definition-sync.js +2 -150
- package/dist/scope/registry-artifact-sync.d.ts +3 -0
- package/dist/scope/registry-artifact-sync.js +159 -0
- package/dist/scope/specification-sync.d.ts +3 -0
- package/dist/scope/specification-sync.js +16 -0
- package/dist/scope/transition.js +40 -12
- package/dist/session/cache-recovery.d.ts +19 -0
- package/dist/session/cache-recovery.js +26 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +14 -3
- package/dist/session/verify-session-ritual.d.ts +1 -0
- package/dist/session/verify-session-ritual.js +1 -0
- package/dist/triage/welcome/default-mode.js +2 -0
- package/dist/triage/welcome/onboard.js +2 -0
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +9 -0
- package/dist/vbrief-reconcile/labels.d.ts +3 -0
- package/dist/vbrief-reconcile/labels.js +14 -0
- package/dist/vbrief-reconcile/main.d.ts +1 -0
- package/dist/vbrief-reconcile/main.js +13 -3
- package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
- package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
- package/dist/vbrief-reconcile/repo-guard.js +33 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
- package/dist/vbrief-reconcile/umbrellas.js +17 -1
- package/dist/verify-env/agent-hooks.js +4 -3
- package/dist/verify-env/verify-hooks-installed.js +5 -5
- package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
- package/dist/vitest-runner/coverage-debt.d.ts +50 -0
- package/dist/vitest-runner/coverage-debt.js +155 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
- package/dist/xbrief-migrate/detect.js +46 -1
- package/dist/xbrief-migrate/migrate-project.js +19 -4
- package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
- package/dist/xbrief-migrate/migration-containment.js +37 -0
- package/package.json +7 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type GitRunner } from "./git.js";
|
|
2
2
|
import { type DirectivePosture } from "./posture.js";
|
|
3
|
+
export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
|
|
3
4
|
export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
|
|
4
5
|
export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
|
|
5
6
|
export declare const GATED_ENTRYPOINT_COMMANDS: Readonly<Record<string, readonly string[]>>;
|
|
@@ -7,6 +7,7 @@ import { defaultRitualRunner } from "./ritual-entrypoint.js";
|
|
|
7
7
|
import { readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
|
|
8
8
|
import { GATED_STEPS, QUICK_STEPS } from "./session-start.js";
|
|
9
9
|
import { resolveSessionRitualStalenessHours } from "./staleness.js";
|
|
10
|
+
export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
|
|
10
11
|
export const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
|
|
11
12
|
export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
|
|
12
13
|
export const GATED_ENTRYPOINT_COMMANDS = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { ensureTriageCacheHydrated } from "../../cache/empty-populate.js";
|
|
2
3
|
import { maybeSelfHealCache } from "../../cache/fetch.js";
|
|
3
4
|
import { FIRST_TIME_NUDGE, INCOMPLETE_NUDGE_TEMPLATE } from "./constants.js";
|
|
4
5
|
import { classifyOnboarding, detectPriorState } from "./prior-state.js";
|
|
@@ -32,6 +33,7 @@ export function runDefaultMode(projectRoot, options = {}) {
|
|
|
32
33
|
};
|
|
33
34
|
const heal = options.selfHealFn ??
|
|
34
35
|
((root) => {
|
|
36
|
+
ensureTriageCacheHydrated(resolve(root));
|
|
35
37
|
maybeSelfHealCache(resolve(root));
|
|
36
38
|
});
|
|
37
39
|
heal(projectRoot);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { ensureTriageCacheHydrated } from "../../cache/empty-populate.js";
|
|
3
4
|
import { maybeSelfHealCache } from "../../cache/fetch.js";
|
|
4
5
|
import { resolveProjectDefinitionPath } from "../../layout/resolve.js";
|
|
5
6
|
import { DEFAULT_WIP_CAP, SUBSCRIPTION_PRESETS, TRIAGE_SKILL_PATH } from "./constants.js";
|
|
@@ -57,6 +58,7 @@ export function runOnboardMode(projectRoot, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
const heal = options.selfHealFn ??
|
|
59
60
|
((root) => {
|
|
61
|
+
ensureTriageCacheHydrated(resolve(root));
|
|
60
62
|
maybeSelfHealCache(resolve(root));
|
|
61
63
|
});
|
|
62
64
|
heal(projectRoot);
|
|
@@ -41,6 +41,11 @@ export interface RunTsLaneOptions {
|
|
|
41
41
|
readonly runner?: LaneRunner;
|
|
42
42
|
/** Injected sink for human-facing notices (defaults to stdout). */
|
|
43
43
|
readonly out?: (message: string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Env used to resolve release coverage-debt before sanitizeTsLaneEnv strips
|
|
46
|
+
* DEFT_RELEASE_PREFLIGHT (#2618). Defaults to process.env.
|
|
47
|
+
*/
|
|
48
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
44
49
|
}
|
|
45
50
|
/** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
|
|
46
51
|
export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
|
|
@@ -20,6 +20,7 @@ import { spawnSync } from "node:child_process";
|
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
21
|
import { posix, win32 } from "node:path";
|
|
22
22
|
import { BRANCH_GATE_BYPASS_ENV, RELEASE_PREFLIGHT_ENV } from "../release/constants.js";
|
|
23
|
+
import { resolveCoverageDebtIssue } from "../vitest-runner/coverage-debt.js";
|
|
23
24
|
/** Release Step-5 vars that must not leak into vitest via inherited pnpm env (#2434). */
|
|
24
25
|
const TS_LANE_POISON_ENV_KEYS = [BRANCH_GATE_BYPASS_ENV, RELEASE_PREFLIGHT_ENV];
|
|
25
26
|
/**
|
|
@@ -96,12 +97,20 @@ export function runTsLane(projectRoot, options) {
|
|
|
96
97
|
const { pnpm } = options;
|
|
97
98
|
const runner = options.runner ?? defaultRunner;
|
|
98
99
|
const out = options.out ?? ((message) => process.stdout.write(`${message}\n`));
|
|
100
|
+
// Resolve debt from the pre-sanitize env: sanitizeTsLaneEnv strips
|
|
101
|
+
// DEFT_RELEASE_PREFLIGHT (required for the env-based debt path), so forward
|
|
102
|
+
// --allow-coverage-debt=#N on the vitest argv instead (#2618 / #2573).
|
|
103
|
+
const debt = resolveCoverageDebtIssue([], options.env ?? process.env);
|
|
104
|
+
const debtIssue = debt.kind === "valid" ? debt.issue : null;
|
|
99
105
|
if (!pnpm) {
|
|
100
106
|
out(SKIP_NOTICE);
|
|
101
107
|
return 0;
|
|
102
108
|
}
|
|
103
109
|
for (const command of LANE_COMMANDS) {
|
|
104
110
|
const argv = [pnpm, ...command];
|
|
111
|
+
if (debtIssue !== null && command[1] === "test") {
|
|
112
|
+
argv.push("--", `--allow-coverage-debt=${debtIssue}`);
|
|
113
|
+
}
|
|
105
114
|
const result = runner(argv, projectRoot);
|
|
106
115
|
const code = result.status;
|
|
107
116
|
// A null status means the child was terminated by a signal (SIGKILL / OOM /
|
|
@@ -14,6 +14,9 @@ export interface ReconcileLabelsOptions {
|
|
|
14
14
|
readonly repo?: string | null;
|
|
15
15
|
readonly dryRun?: boolean;
|
|
16
16
|
readonly client?: LabelClient;
|
|
17
|
+
/** Opt in to mutating issues outside the resolved project repo (#2601). */
|
|
18
|
+
readonly allowCrossRepo?: boolean;
|
|
19
|
+
readonly repoAllowlist?: readonly string[];
|
|
17
20
|
}
|
|
18
21
|
export declare function reconcileLabels(projectRoot: string, options?: ReconcileLabelsOptions): [number, ReconcileLabelsOutcome];
|
|
19
22
|
export declare function renderLabelsReport(outcome: ReconcileLabelsOutcome): string;
|
|
@@ -4,6 +4,7 @@ import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "..
|
|
|
4
4
|
import { call } from "../scm/call.js";
|
|
5
5
|
import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
|
|
6
6
|
import { depResolved, RESOLVED_FOLDERS } from "./graph.js";
|
|
7
|
+
import { isRepoMutationAllowed } from "./repo-guard.js";
|
|
7
8
|
import { allScopeIds, asStrList } from "./swarm-deps.js";
|
|
8
9
|
export const SCAN_FOLDERS = ["proposed", "pending", "active"];
|
|
9
10
|
export const MANAGED_LABELS = ["status:blocked", "epic", "status:tracker", "rfc"];
|
|
@@ -163,6 +164,19 @@ export function reconcileLabels(projectRoot, options = {}) {
|
|
|
163
164
|
? metadata.swarm
|
|
164
165
|
: {};
|
|
165
166
|
const desired = computeDesiredLabels(plan, hasUnresolvedDeps(swarm, knownIds));
|
|
167
|
+
// Guard before any SCM read so cross-repo refusal is explicit (#2601 / Greptile P1).
|
|
168
|
+
const mutateGate = isRepoMutationAllowed(effectiveRepo, root, {
|
|
169
|
+
allowCrossRepo: options.allowCrossRepo,
|
|
170
|
+
allowlist: options.repoAllowlist,
|
|
171
|
+
explicitRepo: options.repo ?? null,
|
|
172
|
+
});
|
|
173
|
+
if (!mutateGate.allowed) {
|
|
174
|
+
outcome.errors.push({
|
|
175
|
+
story_id: storyId,
|
|
176
|
+
message: mutateGate.reason ?? `refusing cross-repo mutation on ${effectiveRepo}`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
166
180
|
let current;
|
|
167
181
|
try {
|
|
168
182
|
current = client.fetchLabels(effectiveRepo, number);
|
|
@@ -28,6 +28,8 @@ function parseOptions(rest) {
|
|
|
28
28
|
}
|
|
29
29
|
else if (arg === "--force")
|
|
30
30
|
opts.force = true;
|
|
31
|
+
else if (arg === "--allow-cross-repo")
|
|
32
|
+
opts.allowCrossRepo = true;
|
|
31
33
|
else if (arg === "--dry-run")
|
|
32
34
|
opts.dryRun = true;
|
|
33
35
|
else if (arg === "--json")
|
|
@@ -46,7 +48,7 @@ function parseOptions(rest) {
|
|
|
46
48
|
return opts;
|
|
47
49
|
}
|
|
48
50
|
export function usage() {
|
|
49
|
-
process.stderr.write("usage: vbrief-reconcile <graph|labels|umbrellas> [--project-root PATH] [--dry-run] [--json] [--force] [--repo OWNER/NAME]\n" +
|
|
51
|
+
process.stderr.write("usage: vbrief-reconcile <graph|labels|umbrellas> [--project-root PATH] [--dry-run] [--json] [--force] [--repo OWNER/NAME] [--allow-cross-repo]\n" +
|
|
50
52
|
" vbrief-reconcile parity --scenario NAME [--fixture-root PATH]\n" +
|
|
51
53
|
" vbrief-reconcile parity --all [--fixture-root PATH]\n");
|
|
52
54
|
}
|
|
@@ -70,7 +72,11 @@ export function runGraph(opts) {
|
|
|
70
72
|
}
|
|
71
73
|
export function runLabels(opts) {
|
|
72
74
|
const root = resolve(opts.projectRoot ?? ".");
|
|
73
|
-
const [code, outcome] = reconcileLabels(root, {
|
|
75
|
+
const [code, outcome] = reconcileLabels(root, {
|
|
76
|
+
repo: opts.repo,
|
|
77
|
+
dryRun: opts.dryRun,
|
|
78
|
+
allowCrossRepo: opts.allowCrossRepo,
|
|
79
|
+
});
|
|
74
80
|
if (code === 2) {
|
|
75
81
|
if (opts.json) {
|
|
76
82
|
process.stdout.write(`${JSON.stringify({ error: "no vbrief/ directory found" })}\n`);
|
|
@@ -88,7 +94,11 @@ export function runLabels(opts) {
|
|
|
88
94
|
}
|
|
89
95
|
export function runUmbrellas(opts) {
|
|
90
96
|
const root = resolve(opts.projectRoot ?? ".");
|
|
91
|
-
const [code, outcome] = reconcileUmbrellas(root, {
|
|
97
|
+
const [code, outcome] = reconcileUmbrellas(root, {
|
|
98
|
+
repo: opts.repo,
|
|
99
|
+
dryRun: opts.dryRun,
|
|
100
|
+
allowCrossRepo: opts.allowCrossRepo,
|
|
101
|
+
});
|
|
92
102
|
if (code === 2) {
|
|
93
103
|
if (opts.json) {
|
|
94
104
|
process.stdout.write(`${JSON.stringify({ error: "no vbrief/ directory found" })}\n`);
|
|
@@ -291,7 +291,11 @@ export function runParityScenario(name, options) {
|
|
|
291
291
|
],
|
|
292
292
|
});
|
|
293
293
|
const client = options.labelClient ?? new MemoryLabelClient();
|
|
294
|
-
const [code, outcome] = reconcileLabels(root, {
|
|
294
|
+
const [code, outcome] = reconcileLabels(root, {
|
|
295
|
+
client,
|
|
296
|
+
dryRun: true,
|
|
297
|
+
repo: "deftai/directive",
|
|
298
|
+
});
|
|
295
299
|
return {
|
|
296
300
|
scenario: name,
|
|
297
301
|
ok: true,
|
|
@@ -314,7 +318,11 @@ export function runParityScenario(name, options) {
|
|
|
314
318
|
],
|
|
315
319
|
});
|
|
316
320
|
const client = options.labelClient ?? new MemoryLabelClient();
|
|
317
|
-
const [code, outcome] = reconcileLabels(root, {
|
|
321
|
+
const [code, outcome] = reconcileLabels(root, {
|
|
322
|
+
client,
|
|
323
|
+
dryRun: true,
|
|
324
|
+
repo: "deftai/directive",
|
|
325
|
+
});
|
|
318
326
|
return {
|
|
319
327
|
scenario: name,
|
|
320
328
|
ok: true,
|
|
@@ -342,6 +350,7 @@ export function runParityScenario(name, options) {
|
|
|
342
350
|
client,
|
|
343
351
|
dryRun: true,
|
|
344
352
|
now: FIXED_NOW.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
353
|
+
repo: "deftai/directive",
|
|
345
354
|
});
|
|
346
355
|
return {
|
|
347
356
|
scenario: name,
|
|
@@ -367,8 +376,13 @@ export function runParityScenario(name, options) {
|
|
|
367
376
|
});
|
|
368
377
|
const client = options.umbrellaClient ?? SHARED_UMBRELLA_CLIENT;
|
|
369
378
|
const now = FIXED_NOW.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
370
|
-
reconcileUmbrellas(root, { client, dryRun: false, now });
|
|
371
|
-
const [code, outcome] = reconcileUmbrellas(root, {
|
|
379
|
+
reconcileUmbrellas(root, { client, dryRun: false, now, repo: "deftai/directive" });
|
|
380
|
+
const [code, outcome] = reconcileUmbrellas(root, {
|
|
381
|
+
client,
|
|
382
|
+
dryRun: true,
|
|
383
|
+
now,
|
|
384
|
+
repo: "deftai/directive",
|
|
385
|
+
});
|
|
372
386
|
return {
|
|
373
387
|
scenario: name,
|
|
374
388
|
ok: true,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface RepoMutateGuardOptions {
|
|
2
|
+
readonly allowCrossRepo?: boolean;
|
|
3
|
+
readonly allowlist?: readonly string[];
|
|
4
|
+
readonly explicitRepo?: string | null;
|
|
5
|
+
}
|
|
6
|
+
/** Normalize owner/name slugs for case-insensitive comparison. */
|
|
7
|
+
export declare function normalizeRepoSlug(repo: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Fail-closed gate before SCM label/comment mutations (#2601).
|
|
10
|
+
* Cross-repo targets are refused unless explicitly opted in or allowlisted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isRepoMutationAllowed(targetRepo: string, projectRoot: string, options?: RepoMutateGuardOptions): {
|
|
13
|
+
readonly allowed: boolean;
|
|
14
|
+
readonly projectRepo: string | null;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=repo-guard.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolveRepo } from "../triage/queue/repo.js";
|
|
2
|
+
/** Normalize owner/name slugs for case-insensitive comparison. */
|
|
3
|
+
export function normalizeRepoSlug(repo) {
|
|
4
|
+
return repo.trim().toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fail-closed gate before SCM label/comment mutations (#2601).
|
|
8
|
+
* Cross-repo targets are refused unless explicitly opted in or allowlisted.
|
|
9
|
+
*/
|
|
10
|
+
export function isRepoMutationAllowed(targetRepo, projectRoot, options = {}) {
|
|
11
|
+
const projectRepo = resolveRepo(options.explicitRepo ?? null, projectRoot);
|
|
12
|
+
if (options.allowCrossRepo) {
|
|
13
|
+
return { allowed: true, projectRepo };
|
|
14
|
+
}
|
|
15
|
+
const normalizedTarget = normalizeRepoSlug(targetRepo);
|
|
16
|
+
for (const entry of options.allowlist ?? []) {
|
|
17
|
+
if (normalizeRepoSlug(entry) === normalizedTarget) {
|
|
18
|
+
return { allowed: true, projectRepo };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (projectRepo !== null && normalizeRepoSlug(projectRepo) === normalizedTarget) {
|
|
22
|
+
return { allowed: true, projectRepo };
|
|
23
|
+
}
|
|
24
|
+
const hint = projectRepo !== null
|
|
25
|
+
? `(project repo: ${projectRepo})`
|
|
26
|
+
: "(project repo unknown — set --repo or git remote origin)";
|
|
27
|
+
return {
|
|
28
|
+
allowed: false,
|
|
29
|
+
projectRepo,
|
|
30
|
+
reason: `refusing cross-repo mutation on ${targetRepo} ${hint}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=repo-guard.js.map
|
|
@@ -41,6 +41,9 @@ export interface ReconcileUmbrellasOptions {
|
|
|
41
41
|
readonly dryRun?: boolean;
|
|
42
42
|
readonly client?: UmbrellaClient;
|
|
43
43
|
readonly now?: string;
|
|
44
|
+
/** Opt in to mutating comments on issues outside the resolved project repo (#2601). */
|
|
45
|
+
readonly allowCrossRepo?: boolean;
|
|
46
|
+
readonly repoAllowlist?: readonly string[];
|
|
44
47
|
}
|
|
45
48
|
export declare function reconcileUmbrellas(projectRoot: string, options?: ReconcileUmbrellasOptions): [number, ReconcileUmbrellasOutcome];
|
|
46
49
|
export declare function renderUmbrellasReport(outcome: ReconcileUmbrellasOutcome): string;
|
|
@@ -5,6 +5,7 @@ import { createIssueComment, editIssueCommentBody, GitHubBodyError, } from "../i
|
|
|
5
5
|
import { hasArtifactSuffix, resolveLifecycleRoot, stripArtifactSuffix } from "../layout/resolve.js";
|
|
6
6
|
import { call } from "../scm/call.js";
|
|
7
7
|
import { extractIssueRef } from "../triage/reconcile/parse-uri.js";
|
|
8
|
+
import { isRepoMutationAllowed } from "./repo-guard.js";
|
|
8
9
|
export const OPEN_FOLDERS = ["proposed", "pending", "active"];
|
|
9
10
|
export const CLOSED_FOLDERS = ["completed", "cancelled"];
|
|
10
11
|
export const LIFECYCLE_FOLDERS = [...OPEN_FOLDERS, ...CLOSED_FOLDERS];
|
|
@@ -261,6 +262,14 @@ function planShape(epicData, index) {
|
|
|
261
262
|
return [openChildren, closedChildren, waves];
|
|
262
263
|
}
|
|
263
264
|
function reconcileOneEpic(epicData, index, options) {
|
|
265
|
+
const mutateGate = isRepoMutationAllowed(options.repo, options.projectRoot, {
|
|
266
|
+
allowCrossRepo: options.allowCrossRepo,
|
|
267
|
+
allowlist: options.repoAllowlist,
|
|
268
|
+
explicitRepo: options.explicitRepo ?? null,
|
|
269
|
+
});
|
|
270
|
+
if (!mutateGate.allowed) {
|
|
271
|
+
throw new Error(mutateGate.reason ?? `refusing cross-repo mutation on ${options.repo}`);
|
|
272
|
+
}
|
|
264
273
|
const [openChildren, closedChildren, waves] = planShape(epicData, index);
|
|
265
274
|
const total = openChildren.length + closedChildren.length;
|
|
266
275
|
const comments = options.client.fetchComments(options.repo, options.number);
|
|
@@ -412,6 +421,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
|
|
|
412
421
|
client,
|
|
413
422
|
dryRun: options.dryRun ?? false,
|
|
414
423
|
now,
|
|
424
|
+
projectRoot: root,
|
|
425
|
+
allowCrossRepo: options.allowCrossRepo ?? false,
|
|
426
|
+
repoAllowlist: options.repoAllowlist,
|
|
427
|
+
explicitRepo: options.repo ?? null,
|
|
415
428
|
});
|
|
416
429
|
if (change.action === "unchanged")
|
|
417
430
|
outcome.unchanged.push(change);
|
|
@@ -419,7 +432,10 @@ export function reconcileUmbrellas(projectRoot, options = {}) {
|
|
|
419
432
|
outcome.changed.push(change);
|
|
420
433
|
}
|
|
421
434
|
catch (exc) {
|
|
422
|
-
outcome.errors.push({
|
|
435
|
+
outcome.errors.push({
|
|
436
|
+
story_id: storyId,
|
|
437
|
+
message: exc instanceof Error ? exc.message : String(exc),
|
|
438
|
+
});
|
|
423
439
|
}
|
|
424
440
|
}
|
|
425
441
|
}
|
|
@@ -25,7 +25,7 @@ export function evaluateAgentHooks(projectRoot) {
|
|
|
25
25
|
if (unhealthy.length > 0) {
|
|
26
26
|
return {
|
|
27
27
|
code: 1,
|
|
28
|
-
message: "❌ deft agent
|
|
28
|
+
message: "❌ deft agent hook registration INCOMPLETE:\n" +
|
|
29
29
|
unhealthy
|
|
30
30
|
.map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
|
|
31
31
|
.join("\n") +
|
|
@@ -36,8 +36,9 @@ export function evaluateAgentHooks(projectRoot) {
|
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
38
38
|
code: 0,
|
|
39
|
-
message: "✓ deft agent hooks
|
|
40
|
-
"(SessionStart + PreToolUse direct-write tools only;
|
|
39
|
+
message: "✓ deft agent hooks registered and structurally valid for Claude, Grok, Cursor, Codex " +
|
|
40
|
+
"(SessionStart + PreToolUse direct-write tools only; Codex runtime trust is " +
|
|
41
|
+
"user-controlled and must be reviewed with `/hooks`; shell/MCP policy is deferred).",
|
|
41
42
|
stream: "stdout",
|
|
42
43
|
registrations,
|
|
43
44
|
};
|
|
@@ -134,7 +134,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
134
134
|
return {
|
|
135
135
|
code: 1,
|
|
136
136
|
message: "❌ deft hooks not installed: core.hooksPath is unset.\n" +
|
|
137
|
-
" Recovery: run `
|
|
137
|
+
" Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
|
|
138
138
|
stream: "stderr",
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -147,7 +147,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
147
147
|
code: 1,
|
|
148
148
|
message: `❌ deft hooks wired but NON-FUNCTIONAL: core.hooksPath=${hooksPath} ` +
|
|
149
149
|
`but the directory ${hooksDir} does not exist (#1463 false-green).\n` +
|
|
150
|
-
" Recovery:
|
|
150
|
+
" Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
|
|
151
151
|
stream: "stderr",
|
|
152
152
|
};
|
|
153
153
|
}
|
|
@@ -157,7 +157,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
157
157
|
code: 1,
|
|
158
158
|
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} is missing ` +
|
|
159
159
|
`${missingHooks.join(", ")} (#1463 false-green).\n` +
|
|
160
|
-
" Recovery:
|
|
160
|
+
" Recovery: run `deft update` to deposit project-root `.githooks/`, then `task setup` to wire core.hooksPath.",
|
|
161
161
|
stream: "stderr",
|
|
162
162
|
};
|
|
163
163
|
}
|
|
@@ -169,7 +169,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
169
169
|
message: `❌ deft hooks wired but NON-FUNCTIONAL: ${hooksDir} hook(s) ` +
|
|
170
170
|
`${nonExec.join(", ")} are not executable (git mode is not ` +
|
|
171
171
|
"100755); git silently skips non-executable hooks on Unix (#1477).\n" +
|
|
172
|
-
" Recovery:
|
|
172
|
+
" Recovery: run `deft update` to refresh `.githooks/`, or " +
|
|
173
173
|
"`chmod +x .githooks/pre-commit .githooks/pre-push`.",
|
|
174
174
|
stream: "stderr",
|
|
175
175
|
};
|
|
@@ -198,7 +198,7 @@ export function evaluate(projectRoot, options = {}) {
|
|
|
198
198
|
message: "❌ deft hooks wired but NON-FUNCTIONAL:\n" +
|
|
199
199
|
contentIssues.map((issue) => ` - ${issue.replace(/\r?\n/g, " ")}`).join("\n") +
|
|
200
200
|
"\n" +
|
|
201
|
-
" Recovery:
|
|
201
|
+
" Recovery: run `deft update` to refresh project-root `.githooks/`.",
|
|
202
202
|
stream: "stderr",
|
|
203
203
|
};
|
|
204
204
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { COVERAGE_GOAL, countRecentCoverageDebtMentions, formatCoverageAttribution, formatOveruseWarning, metricsBelowGoal, readCoverageTotalsFromReport, resolveCoverageDebtIssue, } from "./coverage-debt.js";
|
|
4
|
+
/** Vitest globalTeardown: enforce coverage goal or debt soft-pass (#2573). */
|
|
5
|
+
export default async function coverageDebtTeardown() {
|
|
6
|
+
const resolution = resolveCoverageDebtIssue(process.argv, process.env);
|
|
7
|
+
if (resolution.kind === "none")
|
|
8
|
+
return;
|
|
9
|
+
if (resolution.kind === "invalid") {
|
|
10
|
+
throw new Error(`coverage-debt: ${resolution.reason}`);
|
|
11
|
+
}
|
|
12
|
+
const repoRoot = join(import.meta.dirname, "..", "..", "..", "..");
|
|
13
|
+
const totals = readCoverageTotalsFromReport(join(repoRoot, "coverage"));
|
|
14
|
+
if (totals === null) {
|
|
15
|
+
throw new Error("coverage-debt: could not read coverage/coverage-final.json");
|
|
16
|
+
}
|
|
17
|
+
const missed = metricsBelowGoal(totals);
|
|
18
|
+
if (missed.length === 0) {
|
|
19
|
+
process.stderr.write(`coverage-debt: note — --allow-coverage-debt=#${resolution.issue} set but all metrics meet the ${COVERAGE_GOAL.branches}% goal\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`${formatCoverageAttribution(resolution.issue, totals)}\n`);
|
|
23
|
+
try {
|
|
24
|
+
const changelog = readFileSync(join(repoRoot, "CHANGELOG.md"), "utf8");
|
|
25
|
+
const overuse = formatOveruseWarning(countRecentCoverageDebtMentions(changelog));
|
|
26
|
+
if (overuse)
|
|
27
|
+
process.stderr.write(`${overuse}\n`);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// CHANGELOG unreadable — debt attribution still stands.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=coverage-debt-teardown.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shared coverage goal (vitest thresholds). */
|
|
2
|
+
export declare const COVERAGE_GOAL: {
|
|
3
|
+
readonly lines: 85;
|
|
4
|
+
readonly functions: 85;
|
|
5
|
+
readonly branches: 85;
|
|
6
|
+
readonly statements: 85;
|
|
7
|
+
};
|
|
8
|
+
export type CoverageMetric = keyof typeof COVERAGE_GOAL;
|
|
9
|
+
export interface CoverageTotals {
|
|
10
|
+
readonly lines: number;
|
|
11
|
+
readonly functions: number;
|
|
12
|
+
readonly branches: number;
|
|
13
|
+
readonly statements: number;
|
|
14
|
+
}
|
|
15
|
+
export type CoverageDebtResolution = {
|
|
16
|
+
readonly kind: "none";
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "valid";
|
|
19
|
+
readonly issue: number;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "invalid";
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
};
|
|
24
|
+
/** Parse `#2573`, `2573`, or bare numeric strings. */
|
|
25
|
+
export declare function parseCoverageDebtIssueNumber(raw: string): number | null;
|
|
26
|
+
/** Parse `--allow-coverage-debt=#N` tokens from argv. */
|
|
27
|
+
export declare function parseCoverageDebtArgv(argv: readonly string[]): CoverageDebtResolution;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve coverage-debt issue from argv or release-scoped env (#2573 / #1553).
|
|
30
|
+
* Raw env bypass is accepted only during release Step-5 preflight.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveCoverageDebtIssue(argv: readonly string[], env?: NodeJS.ProcessEnv): CoverageDebtResolution;
|
|
33
|
+
interface IstanbulBranchHits {
|
|
34
|
+
readonly [branchId: string]: readonly number[];
|
|
35
|
+
}
|
|
36
|
+
interface IstanbulFileCoverage {
|
|
37
|
+
readonly s?: Readonly<Record<string, number>>;
|
|
38
|
+
readonly f?: Readonly<Record<string, number>>;
|
|
39
|
+
readonly b?: IstanbulBranchHits;
|
|
40
|
+
}
|
|
41
|
+
/** Aggregate global coverage percentages from istanbul coverage-final.json. */
|
|
42
|
+
export declare function summarizeCoverageFinal(coverageFinal: Readonly<Record<string, IstanbulFileCoverage>>): CoverageTotals;
|
|
43
|
+
export declare function readCoverageTotalsFromReport(coverageDir: string): CoverageTotals | null;
|
|
44
|
+
export declare function metricsBelowGoal(totals: CoverageTotals): CoverageMetric[];
|
|
45
|
+
export declare function formatCoverageAttribution(issue: number, totals: CoverageTotals): string;
|
|
46
|
+
/** Count recent release sections citing coverage-debt (CHANGELOG scan). */
|
|
47
|
+
export declare function countRecentCoverageDebtMentions(changelog: string, maxSections?: number): number;
|
|
48
|
+
export declare function formatOveruseWarning(recentCount: number): string | null;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=coverage-debt.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** Shared coverage goal (vitest thresholds). */
|
|
4
|
+
export const COVERAGE_GOAL = {
|
|
5
|
+
lines: 85,
|
|
6
|
+
functions: 85,
|
|
7
|
+
branches: 85,
|
|
8
|
+
statements: 85,
|
|
9
|
+
};
|
|
10
|
+
const DEBT_FLAG = "--allow-coverage-debt";
|
|
11
|
+
const DEBT_ENV = "DEFT_ALLOW_COVERAGE_DEBT";
|
|
12
|
+
const RELEASE_PREFLIGHT_ENV = "DEFT_RELEASE_PREFLIGHT";
|
|
13
|
+
/** Parse `#2573`, `2573`, or bare numeric strings. */
|
|
14
|
+
export function parseCoverageDebtIssueNumber(raw) {
|
|
15
|
+
const trimmed = raw.trim();
|
|
16
|
+
if (!trimmed)
|
|
17
|
+
return null;
|
|
18
|
+
const normalized = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
19
|
+
if (!/^\d+$/.test(normalized))
|
|
20
|
+
return null;
|
|
21
|
+
const issue = Number.parseInt(normalized, 10);
|
|
22
|
+
return Number.isFinite(issue) && issue > 0 ? issue : null;
|
|
23
|
+
}
|
|
24
|
+
/** Parse `--allow-coverage-debt=#N` tokens from argv. */
|
|
25
|
+
export function parseCoverageDebtArgv(argv) {
|
|
26
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
27
|
+
const token = argv[i] ?? "";
|
|
28
|
+
if (token === DEBT_FLAG) {
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
if (next === undefined || next.startsWith("-")) {
|
|
31
|
+
return { kind: "invalid", reason: `${DEBT_FLAG} requires an issue number (#N)` };
|
|
32
|
+
}
|
|
33
|
+
const issue = parseCoverageDebtIssueNumber(next);
|
|
34
|
+
return issue === null
|
|
35
|
+
? { kind: "invalid", reason: `${DEBT_FLAG} value must be #N or N` }
|
|
36
|
+
: { kind: "valid", issue };
|
|
37
|
+
}
|
|
38
|
+
if (token.startsWith(`${DEBT_FLAG}=`)) {
|
|
39
|
+
const value = token.slice(DEBT_FLAG.length + 1);
|
|
40
|
+
const issue = parseCoverageDebtIssueNumber(value);
|
|
41
|
+
return issue === null
|
|
42
|
+
? { kind: "invalid", reason: `${DEBT_FLAG}= value must be #N or N` }
|
|
43
|
+
: { kind: "valid", issue };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { kind: "none" };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve coverage-debt issue from argv or release-scoped env (#2573 / #1553).
|
|
50
|
+
* Raw env bypass is accepted only during release Step-5 preflight.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveCoverageDebtIssue(argv, env = process.env) {
|
|
53
|
+
const fromArgv = parseCoverageDebtArgv(argv);
|
|
54
|
+
if (fromArgv.kind !== "none")
|
|
55
|
+
return fromArgv;
|
|
56
|
+
const rawEnv = env[DEBT_ENV];
|
|
57
|
+
if (rawEnv && env[RELEASE_PREFLIGHT_ENV] === "1") {
|
|
58
|
+
const issue = parseCoverageDebtIssueNumber(rawEnv);
|
|
59
|
+
return issue === null
|
|
60
|
+
? { kind: "invalid", reason: `${DEBT_ENV} must be a positive integer` }
|
|
61
|
+
: { kind: "valid", issue };
|
|
62
|
+
}
|
|
63
|
+
return { kind: "none" };
|
|
64
|
+
}
|
|
65
|
+
/** Aggregate global coverage percentages from istanbul coverage-final.json. */
|
|
66
|
+
export function summarizeCoverageFinal(coverageFinal) {
|
|
67
|
+
let stmtTotal = 0;
|
|
68
|
+
let stmtCovered = 0;
|
|
69
|
+
let fnTotal = 0;
|
|
70
|
+
let fnCovered = 0;
|
|
71
|
+
let branchTotal = 0;
|
|
72
|
+
let branchCovered = 0;
|
|
73
|
+
for (const file of Object.values(coverageFinal)) {
|
|
74
|
+
if (file.s) {
|
|
75
|
+
for (const hits of Object.values(file.s)) {
|
|
76
|
+
stmtTotal += 1;
|
|
77
|
+
if (hits > 0)
|
|
78
|
+
stmtCovered += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (file.f) {
|
|
82
|
+
for (const hits of Object.values(file.f)) {
|
|
83
|
+
fnTotal += 1;
|
|
84
|
+
if (hits > 0)
|
|
85
|
+
fnCovered += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (file.b) {
|
|
89
|
+
for (const paths of Object.values(file.b)) {
|
|
90
|
+
for (const hits of paths) {
|
|
91
|
+
branchTotal += 1;
|
|
92
|
+
if (hits > 0)
|
|
93
|
+
branchCovered += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const pct = (covered, total) => total === 0 ? 100 : (covered / total) * 100;
|
|
99
|
+
const statements = pct(stmtCovered, stmtTotal);
|
|
100
|
+
const functions = pct(fnCovered, fnTotal);
|
|
101
|
+
const branches = pct(branchCovered, branchTotal);
|
|
102
|
+
// Istanbul coverage-final.json does not carry pre-aggregated line hit counts —
|
|
103
|
+
// `l` (line map) is optional and its format differs from `s`/`f`/`b`. Using
|
|
104
|
+
// statements as a proxy may overstate line coverage when multiple statements
|
|
105
|
+
// share a single uncovered line.
|
|
106
|
+
return {
|
|
107
|
+
statements,
|
|
108
|
+
functions,
|
|
109
|
+
branches,
|
|
110
|
+
lines: statements, // approximation: actual line coverage may differ
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function readCoverageTotalsFromReport(coverageDir) {
|
|
114
|
+
const finalPath = join(coverageDir, "coverage-final.json");
|
|
115
|
+
try {
|
|
116
|
+
const raw = readFileSync(finalPath, "utf8");
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return summarizeCoverageFinal(parsed);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function metricsBelowGoal(totals) {
|
|
125
|
+
const missed = [];
|
|
126
|
+
for (const metric of Object.keys(COVERAGE_GOAL)) {
|
|
127
|
+
if (totals[metric] + 1e-9 < COVERAGE_GOAL[metric]) {
|
|
128
|
+
missed.push(metric);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return missed;
|
|
132
|
+
}
|
|
133
|
+
const sanitizeMetric = (value) => String(value).replace(/\r?\n/g, " ");
|
|
134
|
+
export function formatCoverageAttribution(issue, totals) {
|
|
135
|
+
const lines = [
|
|
136
|
+
`coverage-debt: soft-pass acknowledged for issue #${issue}`,
|
|
137
|
+
` measured: branches ${sanitizeMetric(totals.branches.toFixed(2))}% | lines ${sanitizeMetric(totals.lines.toFixed(2))}% | functions ${sanitizeMetric(totals.functions.toFixed(2))}% | statements ${sanitizeMetric(totals.statements.toFixed(2))}%`,
|
|
138
|
+
` goal: branches ${sanitizeMetric(COVERAGE_GOAL.branches)}% | lines ${sanitizeMetric(COVERAGE_GOAL.lines)}% | functions ${sanitizeMetric(COVERAGE_GOAL.functions)}% | statements ${sanitizeMetric(COVERAGE_GOAL.statements)}%`,
|
|
139
|
+
];
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
/** Count recent release sections citing coverage-debt (CHANGELOG scan). */
|
|
143
|
+
export function countRecentCoverageDebtMentions(changelog, maxSections = 5) {
|
|
144
|
+
const versionHeader = /^## \[(?!Unreleased)/m;
|
|
145
|
+
const sections = changelog.split(versionHeader).slice(1, maxSections + 1);
|
|
146
|
+
const pattern = /coverage[- ]debt soft[- ]pass|allow-coverage-debt=#?\d/i;
|
|
147
|
+
return sections.filter((section) => pattern.test(section)).length;
|
|
148
|
+
}
|
|
149
|
+
export function formatOveruseWarning(recentCount) {
|
|
150
|
+
if (recentCount < 2)
|
|
151
|
+
return null;
|
|
152
|
+
return (`coverage-debt WARN: ${recentCount} of the last release sections already cite coverage debt — ` +
|
|
153
|
+
"consider restoring real coverage instead of reusing the escape hatch.");
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=coverage-debt.js.map
|