@deftai/directive-core 0.79.0 → 0.79.1
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.
|
@@ -6,7 +6,7 @@ export type HookHost = (typeof HOOK_HOSTS)[number];
|
|
|
6
6
|
export declare const HOOK_EVENTS: readonly ["session.start", "tool.before"];
|
|
7
7
|
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
8
8
|
export type HookVerdict = "allow" | "deny";
|
|
9
|
-
export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-ready";
|
|
9
|
+
export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready";
|
|
10
10
|
export interface HookDecision {
|
|
11
11
|
readonly verdict: HookVerdict;
|
|
12
12
|
readonly code: HookDecisionCode;
|
|
@@ -33,6 +33,18 @@ export interface HookPolicySeams {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
export declare function hookToolName(payload: unknown): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
38
|
+
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function hookWriteTargetPath(payload: unknown): string | null;
|
|
41
|
+
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
42
|
+
export declare function toProjectRelativePosix(projectRoot: string, targetPath: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
45
|
+
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isProposedLifecycleWrite(projectRoot: string, targetPath: string | null): boolean;
|
|
36
48
|
export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
|
|
37
49
|
export declare function isHookHost(value: string): value is HookHost;
|
|
38
50
|
export declare function isHookEvent(value: string): value is HookEvent;
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
2
3
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
3
4
|
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
4
5
|
import { inspectActiveScope } from "./scope.js";
|
|
@@ -24,6 +25,39 @@ export function hookToolName(payload) {
|
|
|
24
25
|
return null;
|
|
25
26
|
return firstString(input.tool_name, input.toolName, input.tool);
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
30
|
+
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
31
|
+
*/
|
|
32
|
+
export function hookWriteTargetPath(payload) {
|
|
33
|
+
const input = record(payload);
|
|
34
|
+
if (input === null)
|
|
35
|
+
return null;
|
|
36
|
+
const toolInput = record(input.tool_input) ?? record(input.toolInput) ?? record(input.input);
|
|
37
|
+
return firstString(toolInput?.file_path, toolInput?.filePath, toolInput?.path, input.file_path, input.filePath, input.path);
|
|
38
|
+
}
|
|
39
|
+
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
40
|
+
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
41
|
+
const abs = resolve(projectRoot, targetPath);
|
|
42
|
+
const rel = relative(resolve(projectRoot), abs);
|
|
43
|
+
return rel.split(sep).join("/");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
47
|
+
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
48
|
+
*/
|
|
49
|
+
export function isProposedLifecycleWrite(projectRoot, targetPath) {
|
|
50
|
+
if (targetPath === null || targetPath.trim().length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
const posix = toProjectRelativePosix(projectRoot, targetPath);
|
|
53
|
+
// resolve()+relative() collapses mid-path `..`; only outside-root `..` remains.
|
|
54
|
+
if (posix.startsWith(".."))
|
|
55
|
+
return false;
|
|
56
|
+
const base = posix.includes("/") ? posix.slice(posix.lastIndexOf("/") + 1) : posix;
|
|
57
|
+
if (!hasArtifactSuffix(base))
|
|
58
|
+
return false;
|
|
59
|
+
return posix.startsWith("xbrief/proposed/") || posix.startsWith("vbrief/proposed/");
|
|
60
|
+
}
|
|
27
61
|
export function projectRootFromHookPayload(payload, fallback) {
|
|
28
62
|
const input = record(payload);
|
|
29
63
|
if (input === null)
|
|
@@ -124,6 +158,20 @@ export function decideHook(input, seams = {}) {
|
|
|
124
158
|
"Recovery: run `deft session:start`, then " +
|
|
125
159
|
"`deft verify:session-ritual -- --tier=gated`.");
|
|
126
160
|
}
|
|
161
|
+
const writeTarget = hookWriteTargetPath(input.payload);
|
|
162
|
+
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
163
|
+
return {
|
|
164
|
+
verdict: "allow",
|
|
165
|
+
code: "write-propose-ready",
|
|
166
|
+
event: input.event,
|
|
167
|
+
host: input.host,
|
|
168
|
+
toolName,
|
|
169
|
+
projectRoot,
|
|
170
|
+
message: `Directive write gate allowed ${toolName} for a proposed lifecycle xBRIEF ` +
|
|
171
|
+
"(planning write; active scope not required).",
|
|
172
|
+
scopePath: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
127
175
|
let scope;
|
|
128
176
|
try {
|
|
129
177
|
scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
|
|
@@ -132,8 +180,15 @@ export function decideHook(input, seams = {}) {
|
|
|
132
180
|
scope = { ready: false, path: null, message: String(cause) };
|
|
133
181
|
}
|
|
134
182
|
if (!scope.ready) {
|
|
135
|
-
|
|
136
|
-
|
|
183
|
+
const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
184
|
+
const proposedPathHint = relTarget !== null &&
|
|
185
|
+
(relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
|
|
186
|
+
? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
|
|
187
|
+
"filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
|
|
188
|
+
"planning writes (#2625)."
|
|
189
|
+
: " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
|
|
190
|
+
"or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption).";
|
|
191
|
+
return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
|
|
137
192
|
}
|
|
138
193
|
return {
|
|
139
194
|
verdict: "allow",
|
|
@@ -684,7 +684,12 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
|
|
|
684
684
|
mkdirSync(join(vbriefDir, destFolder), { recursive: true });
|
|
685
685
|
try {
|
|
686
686
|
statSync(dst);
|
|
687
|
-
|
|
687
|
+
// Destination already holds this basename (#2622). Treat as already-terminal
|
|
688
|
+
// and continue the sweep. Do not unlink the source — a same-basename collision
|
|
689
|
+
// can be a non-identical artifact; leave removal to the operator / git.
|
|
690
|
+
process.stderr.write(`reconcile: skipped ${relPath} — already present in ${destFolder}/ (#2622); ` +
|
|
691
|
+
"left source in place for manual cleanup\n");
|
|
692
|
+
skipped += 1;
|
|
688
693
|
continue;
|
|
689
694
|
}
|
|
690
695
|
catch {
|
|
@@ -17,6 +17,15 @@ export interface CaptureExecOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
|
|
19
19
|
export declare function captureExec(executable: string, args: readonly string[], timeoutMs: number, options?: CaptureExecOptions): CaptureExecResult;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a CLI entry script for subprocess spawn (#2615).
|
|
22
|
+
*
|
|
23
|
+
* Published npm layout nests `@deftai/directive-core` under `@deftai/directive`,
|
|
24
|
+
* so the old `../../../cli/dist` relative path resolved to a non-existent
|
|
25
|
+
* `@deftai/cli` sibling. Prefer the published package root, then monorepo path,
|
|
26
|
+
* then nested `@deftai/directive/dist`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function cliScriptPath(name: string): string;
|
|
20
29
|
export interface RunProtectedCheckOptions {
|
|
21
30
|
readonly nodeExecutable?: string;
|
|
22
31
|
readonly timeout?: number;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
2
4
|
import { dirname, resolve } from "node:path";
|
|
3
5
|
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { resolveBinary } from "../scm/binary.js";
|
|
@@ -40,9 +42,36 @@ export function captureExec(executable, args, timeoutMs, options = {}) {
|
|
|
40
42
|
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a CLI entry script for subprocess spawn (#2615).
|
|
47
|
+
*
|
|
48
|
+
* Published npm layout nests `@deftai/directive-core` under `@deftai/directive`,
|
|
49
|
+
* so the old `../../../cli/dist` relative path resolved to a non-existent
|
|
50
|
+
* `@deftai/cli` sibling. Prefer the published package root, then monorepo path,
|
|
51
|
+
* then nested `@deftai/directive/dist`.
|
|
52
|
+
*/
|
|
53
|
+
export function cliScriptPath(name) {
|
|
54
|
+
const script = `${name}.js`;
|
|
44
55
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
|
|
56
|
+
const candidates = [];
|
|
57
|
+
try {
|
|
58
|
+
const require = createRequire(import.meta.url);
|
|
59
|
+
const pkgJson = require.resolve("@deftai/directive/package.json");
|
|
60
|
+
candidates.push(resolve(dirname(pkgJson), "dist", script));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Core does not declare a runtime dependency on the CLI package; ignore.
|
|
64
|
+
}
|
|
65
|
+
// Monorepo: packages/core/dist/pr-wait-mergeable -> packages/cli/dist
|
|
66
|
+
candidates.push(resolve(here, "../../../cli/dist", script));
|
|
67
|
+
// npm nested: .../directive/node_modules/@deftai/directive-core/dist/... -> .../directive/dist
|
|
68
|
+
candidates.push(resolve(here, "../../../../dist", script));
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
if (existsSync(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
// ENOENT path for actionable errors — monorepo layout is the stable fallback.
|
|
74
|
+
return resolve(here, "../../../cli/dist", script);
|
|
46
75
|
}
|
|
47
76
|
/** Invoke pr-protected-issues CLI and return (returncode, stdout, stderr). */
|
|
48
77
|
export function runProtectedCheck(prNumber, repo, protectedIssues, options = {}) {
|
|
@@ -64,6 +64,8 @@ export const RELEASE_HELP = "usage: release [-h] [--dry-run] [--skip-tag] [--ski
|
|
|
64
64
|
" Acknowledge a hairline coverage miss: Step 5 passes\n" +
|
|
65
65
|
" only when metrics sit below the 85% goal and this\n" +
|
|
66
66
|
" flag cites an operator-owned issue number (#2573).\n" +
|
|
67
|
+
" PowerShell: use --allow-coverage-debt=N (no bare #)\n" +
|
|
68
|
+
' or quote "#N"; unquoted # starts a comment (#2621).\n' +
|
|
67
69
|
" --skip-ci Skip Step 3 (task ci:local / task check fallback).\n" +
|
|
68
70
|
" Used by `task release:e2e` to keep wall-clock\n" +
|
|
69
71
|
" manageable inside the auto-created temp repo (CI\n" +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.79.
|
|
3
|
+
"version": "0.79.1",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -305,8 +305,8 @@
|
|
|
305
305
|
"provenance": true
|
|
306
306
|
},
|
|
307
307
|
"dependencies": {
|
|
308
|
-
"@deftai/directive-content": "^0.79.
|
|
309
|
-
"@deftai/directive-types": "^0.79.
|
|
308
|
+
"@deftai/directive-content": "^0.79.1",
|
|
309
|
+
"@deftai/directive-types": "^0.79.1",
|
|
310
310
|
"archiver": "^8.0.0"
|
|
311
311
|
},
|
|
312
312
|
"scripts": {
|