@deftai/directive-core 0.82.0 → 0.84.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/main.js +36 -2
- package/dist/cache/task-cache/constants.d.ts +4 -0
- package/dist/cache/task-cache/constants.js +4 -0
- package/dist/cache/task-cache/executor.d.ts +9 -0
- package/dist/cache/task-cache/executor.js +51 -0
- package/dist/cache/task-cache/hash.d.ts +17 -0
- package/dist/cache/task-cache/hash.js +92 -0
- package/dist/cache/task-cache/index.d.ts +14 -0
- package/dist/cache/task-cache/index.js +15 -0
- package/dist/cache/task-cache/lint.d.ts +4 -0
- package/dist/cache/task-cache/lint.js +55 -0
- package/dist/cache/task-cache/registry.d.ts +7 -0
- package/dist/cache/task-cache/registry.js +67 -0
- package/dist/cache/task-cache/store.d.ts +10 -0
- package/dist/cache/task-cache/store.js +48 -0
- package/dist/cache/task-cache/types.d.ts +48 -0
- package/dist/cache/task-cache/types.js +3 -0
- package/dist/check/cached-orchestrator.d.ts +16 -0
- package/dist/check/cached-orchestrator.js +76 -0
- package/dist/check/context.d.ts +30 -0
- package/dist/check/context.js +28 -0
- package/dist/check/gate-lists.d.ts +18 -0
- package/dist/check/gate-lists.js +68 -0
- package/dist/check/index.d.ts +4 -1
- package/dist/check/index.js +3 -0
- package/dist/check/orchestrator.d.ts +3 -45
- package/dist/check/orchestrator.js +8 -46
- package/dist/check/runner-detect.d.ts +20 -0
- package/dist/check/runner-detect.js +131 -0
- package/dist/eval/readback.js +6 -1
- package/dist/hooks/cursor-hooks.d.ts +23 -0
- package/dist/hooks/cursor-hooks.js +95 -0
- package/dist/hooks/dispatcher.d.ts +10 -1
- package/dist/hooks/dispatcher.js +66 -6
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -2
- package/dist/init-deposit/agent-hooks.js +137 -14
- package/dist/init-deposit/gitignore.js +1 -0
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/lifecycle/events.d.ts +1 -0
- package/dist/lifecycle/events.js +59 -9
- package/dist/policy/host-hooks.d.ts +21 -0
- package/dist/policy/host-hooks.js +96 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/product-signal/consent.d.ts +20 -3
- package/dist/product-signal/consent.js +63 -6
- package/dist/product-signal/gates.d.ts +1 -1
- package/dist/product-signal/submit.d.ts +5 -1
- package/dist/product-signal/submit.js +42 -18
- package/dist/scope/decompose.js +9 -3
- package/dist/session/git.d.ts +2 -0
- package/dist/session/git.js +14 -0
- package/dist/session/verify-session-ritual.js +39 -5
- package/dist/swarm/routing-set-cli.js +16 -5
- package/dist/swarm/routing.d.ts +1 -1
- package/dist/swarm/routing.js +3 -1
- package/dist/value/readback.js +6 -1
- package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
- package/dist/vbrief-validate/plan-hooks.js +25 -0
- package/dist/verify-env/agent-hooks.d.ts +2 -1
- package/dist/verify-env/agent-hooks.js +3 -2
- package/package.json +7 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
/** True when `path` is the directive framework source checkout root. */
|
|
4
|
+
export function isFrameworkRepoRoot(path) {
|
|
5
|
+
const root = resolve(path);
|
|
6
|
+
return (existsSync(join(root, "packages", "cli", "package.json")) &&
|
|
7
|
+
existsSync(join(root, "biome.json")) &&
|
|
8
|
+
existsSync(join(root, "Taskfile.yml")));
|
|
9
|
+
}
|
|
10
|
+
/** Return true when running in the framework's own source checkout (#1519). */
|
|
11
|
+
export function isFrameworkSourceContext(frameworkRoot, projectRoot) {
|
|
12
|
+
const fw = resolve(frameworkRoot);
|
|
13
|
+
const pr = resolve(projectRoot);
|
|
14
|
+
if (fw === pr) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (!isFrameworkRepoRoot(fw)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return pr.startsWith(`${fw}${sep}`);
|
|
21
|
+
}
|
|
22
|
+
/** Select the Taskfile target for the given context. */
|
|
23
|
+
export function resolveCheckTarget(frameworkRoot, projectRoot) {
|
|
24
|
+
return isFrameworkSourceContext(frameworkRoot, projectRoot)
|
|
25
|
+
? "check:framework-source"
|
|
26
|
+
: "check:consumer";
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Gate execution order mirrors Taskfile.yml check targets (#1713 dogfood). */
|
|
2
|
+
/**
|
|
3
|
+
* A check gate is either a bare Taskfile task name, or a public task plus CLI
|
|
4
|
+
* args (after `--`). Framework-only shims that are `internal: true` in
|
|
5
|
+
* Taskfile.yml cannot be shelled by name under go-task 3.50 (#2791) — use the
|
|
6
|
+
* public surface with the shim's flags instead.
|
|
7
|
+
*/
|
|
8
|
+
export type CheckGateSpec = string | {
|
|
9
|
+
readonly task: string;
|
|
10
|
+
readonly args?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function checkGateId(spec: CheckGateSpec): string;
|
|
13
|
+
/** Args for `task … --taskfile <path>` (optional `--` + public CLI flags). */
|
|
14
|
+
export declare function checkGateSpawnArgs(spec: CheckGateSpec, taskfilePath: string): string[];
|
|
15
|
+
export declare const FRAMEWORK_CHECK_GATES: readonly CheckGateSpec[];
|
|
16
|
+
export declare const CONSUMER_CHECK_GATES: readonly CheckGateSpec[];
|
|
17
|
+
export declare function gatesForCheckTarget(target: string): readonly CheckGateSpec[];
|
|
18
|
+
//# sourceMappingURL=gate-lists.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Gate execution order mirrors Taskfile.yml check targets (#1713 dogfood). */
|
|
2
|
+
export function checkGateId(spec) {
|
|
3
|
+
return typeof spec === "string" ? spec : spec.task;
|
|
4
|
+
}
|
|
5
|
+
/** Args for `task … --taskfile <path>` (optional `--` + public CLI flags). */
|
|
6
|
+
export function checkGateSpawnArgs(spec, taskfilePath) {
|
|
7
|
+
const task = checkGateId(spec);
|
|
8
|
+
const extra = typeof spec === "string" ? undefined : spec.args;
|
|
9
|
+
if (extra !== undefined && extra.length > 0) {
|
|
10
|
+
return [task, "--taskfile", taskfilePath, "--", ...extra];
|
|
11
|
+
}
|
|
12
|
+
return [task, "--taskfile", taskfilePath];
|
|
13
|
+
}
|
|
14
|
+
export const FRAMEWORK_CHECK_GATES = [
|
|
15
|
+
"ts:check-lane",
|
|
16
|
+
"toolchain:check",
|
|
17
|
+
"verify:stubs",
|
|
18
|
+
"verify:links",
|
|
19
|
+
"verify:rule-ownership",
|
|
20
|
+
"verify:biome-config",
|
|
21
|
+
"verify:content-manifest",
|
|
22
|
+
"verify:skill-external-fetch-gate",
|
|
23
|
+
"verify:contract-drift",
|
|
24
|
+
"verify:cursor-tier1",
|
|
25
|
+
"verify:go-freeze",
|
|
26
|
+
"verify:bridge-drift",
|
|
27
|
+
"verify:branch",
|
|
28
|
+
"verify:encoding",
|
|
29
|
+
"verify:forward-coverage",
|
|
30
|
+
"verify:vbrief-conformance",
|
|
31
|
+
"verify:destructive-gh-verbs",
|
|
32
|
+
"verify:scm-boundary",
|
|
33
|
+
"verify:xbrief-drift",
|
|
34
|
+
"verify:no-task-runtime",
|
|
35
|
+
"verify:cache-fresh",
|
|
36
|
+
"verify:pack-drift",
|
|
37
|
+
// Public surface for Taskfile verify-wip-cap-framework-self-check (#1124 / #2791)
|
|
38
|
+
{ task: "verify:wip-cap", args: ["--allow-over-cap"] },
|
|
39
|
+
"verify:orphan-active",
|
|
40
|
+
"verify:agents-md-budget",
|
|
41
|
+
// Public surfaces for internal eval-relocation framework shims (#2791)
|
|
42
|
+
{ task: "verify:eval-health-relocation", args: ["--base-ref", "origin/master"] },
|
|
43
|
+
{ task: "verify:eval-triggers-relocation", args: ["--base-ref", "origin/master"] },
|
|
44
|
+
"vbrief:validate",
|
|
45
|
+
"codebase:validate-structure",
|
|
46
|
+
"verify:codebase-map-fresh",
|
|
47
|
+
"verify-strategy-output",
|
|
48
|
+
];
|
|
49
|
+
export const CONSUMER_CHECK_GATES = [
|
|
50
|
+
"doctor",
|
|
51
|
+
"toolchain:check-consumer",
|
|
52
|
+
"verify:branch",
|
|
53
|
+
"verify:cache-fresh",
|
|
54
|
+
"verify:wip-cap",
|
|
55
|
+
"verify:orphan-active",
|
|
56
|
+
"vbrief:validate",
|
|
57
|
+
"verify-strategy-output",
|
|
58
|
+
];
|
|
59
|
+
export function gatesForCheckTarget(target) {
|
|
60
|
+
if (target === "check:framework-source") {
|
|
61
|
+
return FRAMEWORK_CHECK_GATES;
|
|
62
|
+
}
|
|
63
|
+
if (target === "check:consumer") {
|
|
64
|
+
return CONSUMER_CHECK_GATES;
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=gate-lists.js.map
|
package/dist/check/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
|
+
export { type CheckGateSpec, CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
3
|
+
export type { CheckOrchestratorOptions, CheckOrchestratorSeams } from "./orchestrator.js";
|
|
2
4
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
|
5
|
+
export { detectTestRunner, type RunnerDetectResult, runnerDetectionTable, type TestRunnerKind, } from "./runner-detect.js";
|
|
3
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/check/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
|
+
export { CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
1
3
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
|
4
|
+
export { detectTestRunner, runnerDetectionTable, } from "./runner-detect.js";
|
|
2
5
|
//# sourceMappingURL=index.js.map
|
|
@@ -11,51 +11,9 @@
|
|
|
11
11
|
* 1 -- one or more gates failed
|
|
12
12
|
* 2 -- config error (missing args, task spawn error, etc.)
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
readonly taskBin?: string;
|
|
18
|
-
/** Override the spawnSync implementation for unit testing. */
|
|
19
|
-
readonly spawnFn?: (cmd: string, args: string[], opts: {
|
|
20
|
-
cwd: string;
|
|
21
|
-
stdio: string;
|
|
22
|
-
env?: NodeJS.ProcessEnv;
|
|
23
|
-
timeoutMs?: number;
|
|
24
|
-
}) => {
|
|
25
|
-
status: number | null;
|
|
26
|
-
signal?: NodeJS.Signals | null;
|
|
27
|
-
error?: Error;
|
|
28
|
-
};
|
|
29
|
-
/** Child-process environment (default: process.env). */
|
|
30
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
31
|
-
/** Wall-clock spawn timeout in milliseconds (default: none). */
|
|
32
|
-
readonly timeoutMs?: number;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* True when `path` is the directive framework source checkout root (not a
|
|
36
|
-
* vendored `.deft/core` content deposit). Used to distinguish a maintainer
|
|
37
|
-
* running `task check` from a subdirectory (#2220) from a consumer install.
|
|
38
|
-
*/
|
|
39
|
-
export declare function isFrameworkRepoRoot(path: string): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Return true when running in the framework's own source checkout (#1519).
|
|
42
|
-
*
|
|
43
|
-
* Mirrors `is_framework_source_context` from _project_context.py with one
|
|
44
|
-
* extension (#2220): when the Taskfile lives at the framework repo root,
|
|
45
|
-
* `task check` may be invoked from a subdirectory (`USER_WORKING_DIR` !=
|
|
46
|
-
* `TASKFILE_DIR`). Those invocations must still route to
|
|
47
|
-
* `check:framework-source` so the biome lane runs. We do NOT resolve
|
|
48
|
-
* symlinks on the framework root -- a consumer project may symlink
|
|
49
|
-
* `.deft/core` to a local framework checkout and should still run the
|
|
50
|
-
* consumer-safe gate (the deposit path lacks `packages/cli`).
|
|
51
|
-
*/
|
|
52
|
-
export declare function isFrameworkSourceContext(frameworkRoot: string, projectRoot: string): boolean;
|
|
53
|
-
/**
|
|
54
|
-
* Select the Taskfile target for the given context.
|
|
55
|
-
*
|
|
56
|
-
* Mirrors _project_context.py::dispatch_task_check target selection.
|
|
57
|
-
*/
|
|
58
|
-
export declare function resolveCheckTarget(frameworkRoot: string, projectRoot: string): string;
|
|
14
|
+
import { type CheckOrchestratorSeams } from "./context.js";
|
|
15
|
+
export type { CheckOrchestratorOptions, CheckOrchestratorSeams } from "./context.js";
|
|
16
|
+
export { isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget } from "./context.js";
|
|
59
17
|
/**
|
|
60
18
|
* Dispatch to the context-appropriate `task check` aggregate target.
|
|
61
19
|
*
|
|
@@ -12,52 +12,10 @@
|
|
|
12
12
|
* 2 -- config error (missing args, task spawn error, etc.)
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* vendored `.deft/core` content deposit). Used to distinguish a maintainer
|
|
20
|
-
* running `task check` from a subdirectory (#2220) from a consumer install.
|
|
21
|
-
*/
|
|
22
|
-
export function isFrameworkRepoRoot(path) {
|
|
23
|
-
const root = resolve(path);
|
|
24
|
-
return (existsSync(join(root, "packages", "cli", "package.json")) &&
|
|
25
|
-
existsSync(join(root, "biome.json")) &&
|
|
26
|
-
existsSync(join(root, "Taskfile.yml")));
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Return true when running in the framework's own source checkout (#1519).
|
|
30
|
-
*
|
|
31
|
-
* Mirrors `is_framework_source_context` from _project_context.py with one
|
|
32
|
-
* extension (#2220): when the Taskfile lives at the framework repo root,
|
|
33
|
-
* `task check` may be invoked from a subdirectory (`USER_WORKING_DIR` !=
|
|
34
|
-
* `TASKFILE_DIR`). Those invocations must still route to
|
|
35
|
-
* `check:framework-source` so the biome lane runs. We do NOT resolve
|
|
36
|
-
* symlinks on the framework root -- a consumer project may symlink
|
|
37
|
-
* `.deft/core` to a local framework checkout and should still run the
|
|
38
|
-
* consumer-safe gate (the deposit path lacks `packages/cli`).
|
|
39
|
-
*/
|
|
40
|
-
export function isFrameworkSourceContext(frameworkRoot, projectRoot) {
|
|
41
|
-
const fw = resolve(frameworkRoot);
|
|
42
|
-
const pr = resolve(projectRoot);
|
|
43
|
-
if (fw === pr) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
if (!isFrameworkRepoRoot(fw)) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return pr.startsWith(`${fw}${sep}`);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Select the Taskfile target for the given context.
|
|
53
|
-
*
|
|
54
|
-
* Mirrors _project_context.py::dispatch_task_check target selection.
|
|
55
|
-
*/
|
|
56
|
-
export function resolveCheckTarget(frameworkRoot, projectRoot) {
|
|
57
|
-
return isFrameworkSourceContext(frameworkRoot, projectRoot)
|
|
58
|
-
? "check:framework-source"
|
|
59
|
-
: "check:consumer";
|
|
60
|
-
}
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
import { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
17
|
+
import { resolveCheckTarget } from "./context.js";
|
|
18
|
+
export { isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget } from "./context.js";
|
|
61
19
|
/**
|
|
62
20
|
* Dispatch to the context-appropriate `task check` aggregate target.
|
|
63
21
|
*
|
|
@@ -69,6 +27,10 @@ export function resolveCheckTarget(frameworkRoot, projectRoot) {
|
|
|
69
27
|
export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
|
|
70
28
|
const resolvedFramework = resolve(frameworkRoot);
|
|
71
29
|
const resolvedProject = resolve(projectRoot);
|
|
30
|
+
const useTaskCache = seams.useTaskCache !== false && !seams.noCache;
|
|
31
|
+
if (useTaskCache) {
|
|
32
|
+
return dispatchCachedTaskCheck(resolvedFramework, resolvedProject, seams);
|
|
33
|
+
}
|
|
72
34
|
const taskfilePath = join(resolvedFramework, "Taskfile.yml");
|
|
73
35
|
const taskBin = seams.taskBin ?? "task";
|
|
74
36
|
const target = resolveCheckTarget(resolvedFramework, resolvedProject);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type TestRunnerKind = "vitest" | "jest" | "go" | "pytest" | "none";
|
|
2
|
+
export interface RunnerDetectResult {
|
|
3
|
+
readonly kind: TestRunnerKind;
|
|
4
|
+
readonly affectedArgs: readonly string[];
|
|
5
|
+
readonly source: "config" | "heuristic" | "fallback";
|
|
6
|
+
readonly message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RunnerDetectOptions {
|
|
9
|
+
readonly projectRoot: string;
|
|
10
|
+
readonly override?: TestRunnerKind;
|
|
11
|
+
}
|
|
12
|
+
/** Auto-detect consumer test runner with explicit override (#1713). */
|
|
13
|
+
export declare function detectTestRunner(options: RunnerDetectOptions): RunnerDetectResult;
|
|
14
|
+
/** Human-readable detection table rows for docs. */
|
|
15
|
+
export declare function runnerDetectionTable(): ReadonlyArray<{
|
|
16
|
+
runner: TestRunnerKind;
|
|
17
|
+
detection: string;
|
|
18
|
+
affectedConvention: string;
|
|
19
|
+
}>;
|
|
20
|
+
//# sourceMappingURL=runner-detect.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { projectDefinitionPath } from "../policy/index.js";
|
|
4
|
+
const RUNNER_AFFECTED = {
|
|
5
|
+
vitest: ["--changed"],
|
|
6
|
+
jest: ["--onlyChanged"],
|
|
7
|
+
go: [],
|
|
8
|
+
pytest: ["--testmon"],
|
|
9
|
+
};
|
|
10
|
+
function affectedArgsFor(kind) {
|
|
11
|
+
if (kind === "none") {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return RUNNER_AFFECTED[kind];
|
|
15
|
+
}
|
|
16
|
+
function readJson(path) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function detectFromPolicy(projectRoot) {
|
|
25
|
+
try {
|
|
26
|
+
const planPath = projectDefinitionPath(projectRoot);
|
|
27
|
+
const plan = readJson(planPath);
|
|
28
|
+
const raw = plan?.plan?.policy?.testRunner;
|
|
29
|
+
if (raw === "vitest" || raw === "jest" || raw === "go" || raw === "pytest" || raw === "none") {
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function detectFromHeuristics(projectRoot) {
|
|
39
|
+
const root = resolve(projectRoot);
|
|
40
|
+
if (existsSync(join(root, "go.mod"))) {
|
|
41
|
+
return "go";
|
|
42
|
+
}
|
|
43
|
+
const pkgPath = join(root, "package.json");
|
|
44
|
+
if (existsSync(pkgPath)) {
|
|
45
|
+
const pkg = readJson(pkgPath);
|
|
46
|
+
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
|
|
47
|
+
if ("vitest" in deps) {
|
|
48
|
+
return "vitest";
|
|
49
|
+
}
|
|
50
|
+
if ("jest" in deps || "@jest/core" in deps) {
|
|
51
|
+
return "jest";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(join(root, "pytest.ini")) ||
|
|
55
|
+
existsSync(join(root, "pyproject.toml")) ||
|
|
56
|
+
existsSync(join(root, "requirements.txt"))) {
|
|
57
|
+
return "pytest";
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Auto-detect consumer test runner with explicit override (#1713). */
|
|
62
|
+
export function detectTestRunner(options) {
|
|
63
|
+
if (options.override !== undefined) {
|
|
64
|
+
if (options.override === "none") {
|
|
65
|
+
return {
|
|
66
|
+
kind: "none",
|
|
67
|
+
affectedArgs: [],
|
|
68
|
+
source: "config",
|
|
69
|
+
message: "Runner override set to full-suite-only.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
kind: options.override,
|
|
74
|
+
affectedArgs: affectedArgsFor(options.override),
|
|
75
|
+
source: "config",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const fromPolicy = detectFromPolicy(options.projectRoot);
|
|
79
|
+
if (fromPolicy !== null) {
|
|
80
|
+
return {
|
|
81
|
+
kind: fromPolicy,
|
|
82
|
+
affectedArgs: affectedArgsFor(fromPolicy),
|
|
83
|
+
source: "config",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const fromHeuristics = detectFromHeuristics(options.projectRoot);
|
|
87
|
+
if (fromHeuristics !== null) {
|
|
88
|
+
return {
|
|
89
|
+
kind: fromHeuristics,
|
|
90
|
+
affectedArgs: affectedArgsFor(fromHeuristics),
|
|
91
|
+
source: "heuristic",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: "none",
|
|
96
|
+
affectedArgs: [],
|
|
97
|
+
source: "fallback",
|
|
98
|
+
message: "No supported test runner detected — merge gate uses the full suite.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Human-readable detection table rows for docs. */
|
|
102
|
+
export function runnerDetectionTable() {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
runner: "vitest",
|
|
106
|
+
detection: "package.json lists vitest, or plan.policy.testRunner = vitest",
|
|
107
|
+
affectedConvention: "vitest --changed",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
runner: "jest",
|
|
111
|
+
detection: "package.json lists jest / @jest/core, or plan.policy.testRunner = jest",
|
|
112
|
+
affectedConvention: "jest --onlyChanged",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
runner: "go",
|
|
116
|
+
detection: "go.mod present, or plan.policy.testRunner = go",
|
|
117
|
+
affectedConvention: "go test (native package cache)",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
runner: "pytest",
|
|
121
|
+
detection: "pytest.ini / pyproject.toml / requirements.txt, or plan.policy.testRunner = pytest",
|
|
122
|
+
affectedConvention: "pytest --testmon",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
runner: "none",
|
|
126
|
+
detection: "No match after config + heuristics",
|
|
127
|
+
affectedConvention: "Full suite at merge gate",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=runner-detect.js.map
|
package/dist/eval/readback.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
3
4
|
import { MAX_LINE_CHARS } from "../triage/welcome/constants.js";
|
|
4
5
|
import { evaluateHealth, healthHistoryPath } from "./health.js";
|
|
5
6
|
/** Repeat-suppression window for the budgeted eval session nudge (#1703 / #1279 parity). */
|
|
@@ -124,10 +125,14 @@ function appendEvalReadbackHistory(projectRoot, nudgeKey, line, options = {}) {
|
|
|
124
125
|
line,
|
|
125
126
|
};
|
|
126
127
|
try {
|
|
128
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
127
129
|
mkdirSync(join(path, ".."), { recursive: true });
|
|
128
130
|
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
129
131
|
}
|
|
130
|
-
catch {
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (err instanceof ProjectionContainmentError) {
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
131
136
|
// observability only
|
|
132
137
|
}
|
|
133
138
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
|
|
2
|
+
export declare const APPLY_PATCH_TOOL_NAMES: readonly ["ApplyPatch", "apply_patch"];
|
|
3
|
+
export declare const APPLY_PATCH_HOOK_MATCHER: string;
|
|
4
|
+
/** Generic Cursor write matcher excludes ApplyPatch — adapter owns it (#2764). */
|
|
5
|
+
export declare const CURSOR_GENERIC_WRITE_TOOL_NAMES: ("Edit" | "Write" | "WriteFile" | "CreateFile" | "MultiEdit" | "NotebookEdit" | "StrReplace" | "SearchReplace" | "Delete" | "DeleteFile" | "ApplyPatch" | "apply_patch")[];
|
|
6
|
+
export declare const CURSOR_GENERIC_WRITE_HOOK_MATCHER: string;
|
|
7
|
+
export declare const CURSOR_APPLY_PATCH_ADAPTER_RELATIVE = ".cursor/hooks/deft-cursor-hook-adapter.mjs";
|
|
8
|
+
export declare const DEFT_CURSOR_ADAPTER_COMMAND_MARKER = "deft-cursor-hook-adapter.mjs";
|
|
9
|
+
export declare const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = "node .cursor/hooks/deft-cursor-hook-adapter.mjs ApplyPatch";
|
|
10
|
+
/** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
|
|
11
|
+
export declare const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = "#!/usr/bin/env node\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst projectRoot = resolve(process.cwd());\nconst stdin = readFileSync(0, \"utf8\");\n\nfunction deftCommand() {\n for (const candidate of [\"deft\", \"directive\"]) {\n const probe = spawnSync(candidate, [\"--version\"], {\n encoding: \"utf8\",\n stdio: \"ignore\",\n shell: process.platform === \"win32\",\n windowsHide: true,\n });\n if (probe.error === undefined && probe.status === 0) return candidate;\n }\n process.stderr.write(\n \"Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n\",\n );\n process.exit(2);\n}\n\nconst cli = deftCommand();\nconst result = spawnSync(\n cli,\n [\n \"hook:dispatch\",\n \"--host\",\n \"cursor\",\n \"--event\",\n \"tool.before\",\n \"--project-root\",\n projectRoot,\n ],\n {\n input: stdin,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n shell: process.platform === \"win32\",\n windowsHide: true,\n },\n);\n\nif (result.stderr) process.stderr.write(result.stderr);\nif (result.error) {\n process.stderr.write(String(result.error));\n process.exit(2);\n}\nif (result.status !== 0 && result.status !== null) {\n if (result.stdout) process.stdout.write(result.stdout);\n process.exit(result.status);\n}\n// Cursor failClosed treats empty stdout as failure \u2014 normalize allow.\nconst out = (result.stdout ?? \"\").trim();\nprocess.stdout.write((out.length > 0 ? out : '{\"permission\":\"allow\"}') + \"\\n\");\nprocess.exit(0);\n";
|
|
12
|
+
export interface CursorPreToolUseEntry {
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly matcher?: string;
|
|
15
|
+
readonly failClosed?: boolean;
|
|
16
|
+
readonly timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function cursorApplyPatchAdapterEntry(): CursorPreToolUseEntry;
|
|
19
|
+
/** True when generic and adapter matchers share no tool tokens. */
|
|
20
|
+
export declare function cursorApplyPatchMatchersDisjoint(): boolean;
|
|
21
|
+
/** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
|
|
22
|
+
export declare function assertCursorApplyPatchMatchersDisjoint(): void;
|
|
23
|
+
//# sourceMappingURL=cursor-hooks.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DIRECT_WRITE_TOOL_NAMES } from "./tools.js";
|
|
2
|
+
/** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
|
|
3
|
+
export const APPLY_PATCH_TOOL_NAMES = ["ApplyPatch", "apply_patch"];
|
|
4
|
+
export const APPLY_PATCH_HOOK_MATCHER = APPLY_PATCH_TOOL_NAMES.join("|");
|
|
5
|
+
const APPLY_PATCH_TOOLS = new Set(APPLY_PATCH_TOOL_NAMES);
|
|
6
|
+
/** Generic Cursor write matcher excludes ApplyPatch — adapter owns it (#2764). */
|
|
7
|
+
export const CURSOR_GENERIC_WRITE_TOOL_NAMES = DIRECT_WRITE_TOOL_NAMES.filter((name) => !APPLY_PATCH_TOOLS.has(name));
|
|
8
|
+
export const CURSOR_GENERIC_WRITE_HOOK_MATCHER = CURSOR_GENERIC_WRITE_TOOL_NAMES.join("|");
|
|
9
|
+
export const CURSOR_APPLY_PATCH_ADAPTER_RELATIVE = ".cursor/hooks/deft-cursor-hook-adapter.mjs";
|
|
10
|
+
export const DEFT_CURSOR_ADAPTER_COMMAND_MARKER = "deft-cursor-hook-adapter.mjs";
|
|
11
|
+
export const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = `node ${CURSOR_APPLY_PATCH_ADAPTER_RELATIVE} ApplyPatch`;
|
|
12
|
+
/** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
|
|
13
|
+
export const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = `#!/usr/bin/env node
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
const projectRoot = resolve(process.cwd());
|
|
19
|
+
const stdin = readFileSync(0, "utf8");
|
|
20
|
+
|
|
21
|
+
function deftCommand() {
|
|
22
|
+
for (const candidate of ["deft", "directive"]) {
|
|
23
|
+
const probe = spawnSync(candidate, ["--version"], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
stdio: "ignore",
|
|
26
|
+
shell: process.platform === "win32",
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
});
|
|
29
|
+
if (probe.error === undefined && probe.status === 0) return candidate;
|
|
30
|
+
}
|
|
31
|
+
process.stderr.write(
|
|
32
|
+
"Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n",
|
|
33
|
+
);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cli = deftCommand();
|
|
38
|
+
const result = spawnSync(
|
|
39
|
+
cli,
|
|
40
|
+
[
|
|
41
|
+
"hook:dispatch",
|
|
42
|
+
"--host",
|
|
43
|
+
"cursor",
|
|
44
|
+
"--event",
|
|
45
|
+
"tool.before",
|
|
46
|
+
"--project-root",
|
|
47
|
+
projectRoot,
|
|
48
|
+
],
|
|
49
|
+
{
|
|
50
|
+
input: stdin,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
shell: process.platform === "win32",
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
59
|
+
if (result.error) {
|
|
60
|
+
process.stderr.write(String(result.error));
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
if (result.status !== 0 && result.status !== null) {
|
|
64
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
65
|
+
process.exit(result.status);
|
|
66
|
+
}
|
|
67
|
+
// Cursor failClosed treats empty stdout as failure — normalize allow.
|
|
68
|
+
const out = (result.stdout ?? "").trim();
|
|
69
|
+
process.stdout.write((out.length > 0 ? out : '{"permission":"allow"}') + "\\n");
|
|
70
|
+
process.exit(0);
|
|
71
|
+
`;
|
|
72
|
+
export function cursorApplyPatchAdapterEntry() {
|
|
73
|
+
return {
|
|
74
|
+
command: CURSOR_APPLY_PATCH_ADAPTER_COMMAND,
|
|
75
|
+
matcher: APPLY_PATCH_HOOK_MATCHER,
|
|
76
|
+
failClosed: true,
|
|
77
|
+
timeout: 5,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** True when generic and adapter matchers share no tool tokens. */
|
|
81
|
+
export function cursorApplyPatchMatchersDisjoint() {
|
|
82
|
+
const generic = new Set(CURSOR_GENERIC_WRITE_HOOK_MATCHER.split("|"));
|
|
83
|
+
for (const token of APPLY_PATCH_HOOK_MATCHER.split("|")) {
|
|
84
|
+
if (generic.has(token))
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
|
|
90
|
+
export function assertCursorApplyPatchMatchersDisjoint() {
|
|
91
|
+
if (!cursorApplyPatchMatchersDisjoint()) {
|
|
92
|
+
throw new Error("Cursor ApplyPatch and generic direct-write matchers overlap — refusing hook deposit (#2764).");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=cursor-hooks.js.map
|
|
@@ -67,11 +67,20 @@ export declare function toProjectRelativePosix(projectRoot: string, targetPath:
|
|
|
67
67
|
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
68
68
|
*/
|
|
69
69
|
export declare function isProposedLifecycleWrite(projectRoot: string, targetPath: string | null): boolean;
|
|
70
|
+
/** Normalize hook project-root resolution on Windows (doubled drive + MSYS `/c/...`). */
|
|
71
|
+
export declare function normalizeHookProjectRoot(path: string): string;
|
|
70
72
|
export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
|
|
71
73
|
export declare function isHookHost(value: string): value is HookHost;
|
|
72
74
|
export declare function isHookEvent(value: string): value is HookEvent;
|
|
73
75
|
/** Decide a normalized event using only the P0 direct-write policy. */
|
|
74
76
|
export declare function decideHook(input: HookDispatchInput, seams?: HookPolicySeams): HookDecision;
|
|
75
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Render host-facing hook output.
|
|
79
|
+
*
|
|
80
|
+
* Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
|
|
81
|
+
* hook failure and blocks the tool — so Cursor allows must emit explicit
|
|
82
|
+
* `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
|
|
83
|
+
* flow is unchanged.
|
|
84
|
+
*/
|
|
76
85
|
export declare function renderHostDecision(host: HookHost, decision: HookDecision): string;
|
|
77
86
|
//# sourceMappingURL=dispatcher.d.ts.map
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -161,13 +161,62 @@ export function isProposedLifecycleWrite(projectRoot, targetPath) {
|
|
|
161
161
|
return false;
|
|
162
162
|
return posix.startsWith("xbrief/proposed/") || posix.startsWith("vbrief/proposed/");
|
|
163
163
|
}
|
|
164
|
+
function isWindowsDriveOnlyRoot(value) {
|
|
165
|
+
return /^[A-Za-z]:[/\\]?$/.test(value.trim());
|
|
166
|
+
}
|
|
167
|
+
function hookPayloadRootCandidates(input) {
|
|
168
|
+
const candidates = [];
|
|
169
|
+
const push = (value) => {
|
|
170
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
171
|
+
candidates.push(value.trim());
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
push(input.workspaceRoot);
|
|
175
|
+
push(input.workspace_root);
|
|
176
|
+
const workspaceRoots = input.workspace_roots;
|
|
177
|
+
if (Array.isArray(workspaceRoots)) {
|
|
178
|
+
for (const entry of workspaceRoots)
|
|
179
|
+
push(entry);
|
|
180
|
+
}
|
|
181
|
+
push(input.cwd);
|
|
182
|
+
return candidates;
|
|
183
|
+
}
|
|
184
|
+
/** Collapse join('C:', 'c:\\...') doubled drive prefix on Windows (#2787). */
|
|
185
|
+
function collapseDoubledWindowsDrivePrefix(path) {
|
|
186
|
+
return path.replace(/^([A-Za-z]:\\)(?=[A-Za-z]:\\)/i, "");
|
|
187
|
+
}
|
|
188
|
+
function msysPathToWin32(path) {
|
|
189
|
+
const match = /^\/([a-zA-Z])\/(.*)$/.exec(path.trim());
|
|
190
|
+
if (match === null || match[1] === undefined || match[2] === undefined)
|
|
191
|
+
return null;
|
|
192
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
193
|
+
}
|
|
194
|
+
/** Normalize hook project-root resolution on Windows (doubled drive + MSYS `/c/...`). */
|
|
195
|
+
export function normalizeHookProjectRoot(path) {
|
|
196
|
+
if (process.platform !== "win32")
|
|
197
|
+
return resolve(path);
|
|
198
|
+
const trimmed = path.trim();
|
|
199
|
+
const msys = msysPathToWin32(trimmed);
|
|
200
|
+
let resolved = resolve(msys ?? trimmed);
|
|
201
|
+
const collapsed = collapseDoubledWindowsDrivePrefix(resolved);
|
|
202
|
+
if (collapsed !== resolved) {
|
|
203
|
+
resolved = resolve(collapsed);
|
|
204
|
+
}
|
|
205
|
+
if (/^[a-z]:\\/.test(resolved)) {
|
|
206
|
+
resolved = `${resolved.charAt(0).toUpperCase()}${resolved.slice(1)}`;
|
|
207
|
+
}
|
|
208
|
+
return resolved;
|
|
209
|
+
}
|
|
164
210
|
export function projectRootFromHookPayload(payload, fallback) {
|
|
211
|
+
const fallbackResolved = normalizeHookProjectRoot(fallback);
|
|
165
212
|
const input = record(payload);
|
|
166
213
|
if (input === null)
|
|
167
|
-
return
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
return fallbackResolved;
|
|
215
|
+
const usable = hookPayloadRootCandidates(input).find((candidate) => !isWindowsDriveOnlyRoot(candidate));
|
|
216
|
+
if (usable !== undefined) {
|
|
217
|
+
return normalizeHookProjectRoot(usable);
|
|
218
|
+
}
|
|
219
|
+
return fallbackResolved;
|
|
171
220
|
}
|
|
172
221
|
export function isHookHost(value) {
|
|
173
222
|
return HOOK_HOSTS.includes(value);
|
|
@@ -411,10 +460,21 @@ export function decideHook(input, seams = {}) {
|
|
|
411
460
|
}
|
|
412
461
|
return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: true });
|
|
413
462
|
}
|
|
414
|
-
/**
|
|
463
|
+
/**
|
|
464
|
+
* Render host-facing hook output.
|
|
465
|
+
*
|
|
466
|
+
* Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
|
|
467
|
+
* hook failure and blocks the tool — so Cursor allows must emit explicit
|
|
468
|
+
* `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
|
|
469
|
+
* flow is unchanged.
|
|
470
|
+
*/
|
|
415
471
|
export function renderHostDecision(host, decision) {
|
|
416
|
-
if (decision.verdict === "allow")
|
|
472
|
+
if (decision.verdict === "allow") {
|
|
473
|
+
if (host === "cursor") {
|
|
474
|
+
return JSON.stringify({ permission: "allow" });
|
|
475
|
+
}
|
|
417
476
|
return "";
|
|
477
|
+
}
|
|
418
478
|
switch (host) {
|
|
419
479
|
case "claude":
|
|
420
480
|
case "codex":
|