@deftai/directive-core 0.86.0 → 0.87.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/check/gate-lists.js +1 -0
- package/dist/doctor/main.d.ts +6 -5
- package/dist/doctor/main.js +32 -18
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/hooks/dispatcher.d.ts +21 -1
- package/dist/hooks/dispatcher.js +85 -15
- package/dist/intake/issue-emit.d.ts +45 -2
- package/dist/intake/issue-emit.js +420 -17
- package/dist/intake/issue-ingest.js +54 -4
- package/dist/platform/platform-capabilities.js +3 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +6 -2
- package/dist/review-monitor/tier-detection.js +27 -2
- package/dist/scope/transition.js +43 -0
- package/dist/session/release-availability.d.ts +2 -0
- package/dist/session/release-availability.js +23 -8
- package/dist/swarm/routing-set-cli.js +5 -10
- package/dist/swarm/routing.d.ts +3 -2
- package/dist/swarm/routing.js +16 -4
- package/dist/triage/help/registry-data.d.ts +7 -7
- package/dist/triage/help/registry-data.js +15 -6
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/show.d.ts +69 -0
- package/dist/triage/queue/show.js +293 -0
- package/dist/triage/scope/cli.js +3 -0
- package/dist/triage/scope/coverage.d.ts +2 -0
- package/dist/triage/scope/coverage.js +18 -3
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.d.ts +37 -0
- package/dist/verify-source/openclaw-tier1.js +100 -0
- package/dist/xbrief-migrate/migrate-project.js +9 -5
- package/package.json +3 -3
package/dist/check/gate-lists.js
CHANGED
package/dist/doctor/main.d.ts
CHANGED
|
@@ -6,11 +6,12 @@ export declare function runAgentHooksHealthCheck(projectRoot: string, consumerCo
|
|
|
6
6
|
export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
|
|
7
7
|
/**
|
|
8
8
|
* Never emit a bare `task ...` remediation in a project without Taskfile wiring
|
|
9
|
-
* (#2267). The `directive` surface always works; `task deft:X` is
|
|
10
|
-
* only
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* (#2267 / #2893). The `directive`/`deft` surface always works; `task deft:X` is
|
|
10
|
+
* the go-task namespaced form only when the consumer wired the include (bare
|
|
11
|
+
* `task pr:watch` is not the consumer form under include key `deft:`). `plan()`
|
|
12
|
+
* already emits the `directive` / `npx` / `npm` surface, so this guard is a
|
|
13
|
+
* defensive invariant: any `task`-prefixed command is rewritten to the
|
|
14
|
+
* `directive` surface unless the project actually has the include.
|
|
14
15
|
*/
|
|
15
16
|
/**
|
|
16
17
|
* Detect Taskfile wiring through the injected seam (#2267). Mirrors
|
package/dist/doctor/main.js
CHANGED
|
@@ -20,7 +20,7 @@ import { createPlainSink } from "./output.js";
|
|
|
20
20
|
import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
|
|
21
21
|
import { runPayloadStalenessCheck } from "./payload-staleness.js";
|
|
22
22
|
import { runLocalSignpostChecks } from "./signpost-checks.js";
|
|
23
|
-
import { classifyTaskfileInclude, formatMissingIncludeSnippet, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
|
|
23
|
+
import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
|
|
24
24
|
import { defaultWhich } from "./which.js";
|
|
25
25
|
const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
|
|
26
26
|
/**
|
|
@@ -291,7 +291,7 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
291
291
|
if (!jsonMode) {
|
|
292
292
|
sink.blank();
|
|
293
293
|
}
|
|
294
|
-
sink.info("Checking
|
|
294
|
+
sink.info("Checking gates-surface readiness (Taskfile include for deep-think agent gates)...");
|
|
295
295
|
runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams);
|
|
296
296
|
let resolution = null;
|
|
297
297
|
if (!runningInsideDeftRepo(projectRoot, seams)) {
|
|
@@ -640,15 +640,21 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
640
640
|
}
|
|
641
641
|
const includeStatus = classifyTaskfileInclude(projectRoot);
|
|
642
642
|
if (includeStatus === "ok") {
|
|
643
|
-
sink.success("
|
|
643
|
+
sink.success("Gates-surface ready: root Taskfile.yml includes the deft framework (`task deft:<verb>`)");
|
|
644
644
|
return;
|
|
645
645
|
}
|
|
646
646
|
if (includeStatus === "missing-file") {
|
|
647
647
|
let includeMissing = true;
|
|
648
648
|
const target = join(projectRoot, "Taskfile.yml");
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
649
|
+
// #2893: elevate to warning — deep-think agent gates need a working invoke path.
|
|
650
|
+
// Dual remediations: (1) deft CLI primary (2) Taskfile include for task deft: verbs.
|
|
651
|
+
const message = "Gates-surface readiness: root Taskfile.yml missing. Deep-think agent gates " +
|
|
652
|
+
"(`pr:watch`, `review-monitor:*`) need a working invoke path — not optional convenience. " +
|
|
653
|
+
"1. " +
|
|
654
|
+
GATES_SURFACE_DEFT_REMEDIATION +
|
|
655
|
+
` 2. Create ${target} with the canonical include so go-task exposes \`task deft:<verb>\` ` +
|
|
656
|
+
"(include key `deft:` → namespaced tasks; bare `task pr:watch` is not the consumer form):";
|
|
657
|
+
sink.warn(message);
|
|
652
658
|
if (!jsonMode) {
|
|
653
659
|
sink.blank();
|
|
654
660
|
sink.raw(TASKFILE_INCLUDE_SNIPPET);
|
|
@@ -668,22 +674,27 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
668
674
|
}
|
|
669
675
|
}
|
|
670
676
|
else {
|
|
671
|
-
sink.info("Skipped Taskfile.yml creation -- paste the snippet
|
|
677
|
+
sink.info("Skipped Taskfile.yml creation -- use `deft <verb>` now, or paste the include snippet when ready.");
|
|
672
678
|
}
|
|
673
679
|
}
|
|
674
680
|
if (includeMissing) {
|
|
675
681
|
addFinding({
|
|
676
682
|
severity: "warning",
|
|
677
|
-
message: "
|
|
683
|
+
message: "Gates-surface: root Taskfile.yml missing — deep-think gates need `deft` CLI or `task deft:` include",
|
|
678
684
|
check: "taskfile-include",
|
|
679
685
|
file: target,
|
|
680
|
-
suggestion:
|
|
686
|
+
suggestion: formatGatesSurfaceDualRemediation("missing-file"),
|
|
681
687
|
});
|
|
682
688
|
}
|
|
683
689
|
return;
|
|
684
690
|
}
|
|
685
691
|
if (includeStatus === "missing-include") {
|
|
686
|
-
const message = "
|
|
692
|
+
const message = "Gates-surface readiness: root Taskfile.yml exists but does not include the deft framework. " +
|
|
693
|
+
"Deep-think agent gates (`pr:watch`, `review-monitor:*`) need a working invoke path. " +
|
|
694
|
+
"1. " +
|
|
695
|
+
GATES_SURFACE_DEFT_REMEDIATION +
|
|
696
|
+
" 2. Add this to the Taskfile `includes:` block so go-task exposes `task deft:<verb>` " +
|
|
697
|
+
"(doctor NEVER mutates an existing user-owned Taskfile; bare `task pr:watch` is not the consumer form when the include key is `deft:`):";
|
|
687
698
|
sink.warn(message);
|
|
688
699
|
if (!jsonMode) {
|
|
689
700
|
sink.blank();
|
|
@@ -692,21 +703,23 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
692
703
|
const tf = resolveConsumerTaskfile(projectRoot);
|
|
693
704
|
addFinding({
|
|
694
705
|
severity: "warning",
|
|
695
|
-
message: "
|
|
706
|
+
message: "Gates-surface: root Taskfile.yml does not include the deft framework — deep-think gates need `deft` CLI or `task deft:` include",
|
|
696
707
|
check: "taskfile-include",
|
|
697
708
|
file: tf,
|
|
698
|
-
suggestion:
|
|
709
|
+
suggestion: formatGatesSurfaceDualRemediation("missing-include"),
|
|
699
710
|
});
|
|
700
711
|
return;
|
|
701
712
|
}
|
|
702
713
|
const taskfilePath = resolveConsumerTaskfile(projectRoot) ?? join(projectRoot, "Taskfile.yml");
|
|
703
|
-
const message = `
|
|
714
|
+
const message = `Gates-surface readiness: root Taskfile.yml at ${taskfilePath} exists but could not be read — ` +
|
|
715
|
+
"check file permissions. Deep-think gates still work via `deft <verb>` until the include is readable.";
|
|
704
716
|
sink.warn(message);
|
|
705
717
|
addFinding({
|
|
706
718
|
severity: "warning",
|
|
707
719
|
message,
|
|
708
720
|
check: "taskfile-include",
|
|
709
721
|
file: taskfilePath,
|
|
722
|
+
suggestion: GATES_SURFACE_DEFT_REMEDIATION,
|
|
710
723
|
});
|
|
711
724
|
}
|
|
712
725
|
/**
|
|
@@ -786,11 +799,12 @@ function runPlanExtensionShadowCheck(projectRoot, sink, addFinding, seams) {
|
|
|
786
799
|
}
|
|
787
800
|
/**
|
|
788
801
|
* Never emit a bare `task ...` remediation in a project without Taskfile wiring
|
|
789
|
-
* (#2267). The `directive` surface always works; `task deft:X` is
|
|
790
|
-
* only
|
|
791
|
-
* `
|
|
792
|
-
*
|
|
793
|
-
*
|
|
802
|
+
* (#2267 / #2893). The `directive`/`deft` surface always works; `task deft:X` is
|
|
803
|
+
* the go-task namespaced form only when the consumer wired the include (bare
|
|
804
|
+
* `task pr:watch` is not the consumer form under include key `deft:`). `plan()`
|
|
805
|
+
* already emits the `directive` / `npx` / `npm` surface, so this guard is a
|
|
806
|
+
* defensive invariant: any `task`-prefixed command is rewritten to the
|
|
807
|
+
* `directive` surface unless the project actually has the include.
|
|
794
808
|
*/
|
|
795
809
|
/**
|
|
796
810
|
* Detect Taskfile wiring through the injected seam (#2267). Mirrors
|
|
@@ -3,4 +3,12 @@ export declare function resolveConsumerTaskfile(projectRoot: string): string | n
|
|
|
3
3
|
export type TaskfileIncludeStatus = "ok" | "missing-file" | "missing-include" | "unreadable";
|
|
4
4
|
export declare function classifyTaskfileInclude(projectRoot: string): TaskfileIncludeStatus;
|
|
5
5
|
export declare function formatMissingIncludeSnippet(): string;
|
|
6
|
+
/** Primary remediation for deep-think gates when Taskfile include is absent (#2893). */
|
|
7
|
+
export declare const GATES_SURFACE_DEFT_REMEDIATION: string;
|
|
8
|
+
/**
|
|
9
|
+
* Dual remediations for gates-surface readiness (#2893):
|
|
10
|
+
* 1. `deft <verb>` CLI (works without Taskfile)
|
|
11
|
+
* 2. Taskfile include so go-task exposes `task deft:<verb>` (not bare `task pr:watch`)
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatGatesSurfaceDualRemediation(kind: "missing-file" | "missing-include"): string;
|
|
6
14
|
//# sourceMappingURL=taskfile.d.ts.map
|
package/dist/doctor/taskfile.js
CHANGED
|
@@ -62,4 +62,23 @@ export function classifyTaskfileInclude(projectRoot) {
|
|
|
62
62
|
export function formatMissingIncludeSnippet() {
|
|
63
63
|
return " deft:\n taskfile: ./.deft/core/Taskfile.yml\n optional: true\n";
|
|
64
64
|
}
|
|
65
|
+
/** Primary remediation for deep-think gates when Taskfile include is absent (#2893). */
|
|
66
|
+
export const GATES_SURFACE_DEFT_REMEDIATION = "Prefer `deft pr:watch` / `deft review-monitor:register` / `deft verify:review-monitor` " +
|
|
67
|
+
"(primary npm/CLI surface; no Taskfile required).";
|
|
68
|
+
/**
|
|
69
|
+
* Dual remediations for gates-surface readiness (#2893):
|
|
70
|
+
* 1. `deft <verb>` CLI (works without Taskfile)
|
|
71
|
+
* 2. Taskfile include so go-task exposes `task deft:<verb>` (not bare `task pr:watch`)
|
|
72
|
+
*/
|
|
73
|
+
export function formatGatesSurfaceDualRemediation(kind) {
|
|
74
|
+
const includePart = kind === "missing-file"
|
|
75
|
+
? "Create root Taskfile.yml with the canonical deft include so go-task exposes `task deft:<verb>` " +
|
|
76
|
+
"(not bare `task pr:watch`):\n" +
|
|
77
|
+
"version: '3'\n\nincludes:\n" +
|
|
78
|
+
formatMissingIncludeSnippet()
|
|
79
|
+
: "Add the deft include to the existing Taskfile `includes:` block so go-task exposes " +
|
|
80
|
+
"`task deft:<verb>` (doctor NEVER mutates an existing user-owned Taskfile):\n" +
|
|
81
|
+
formatMissingIncludeSnippet();
|
|
82
|
+
return `${GATES_SURFACE_DEFT_REMEDIATION}\n2. ${includePart}`;
|
|
83
|
+
}
|
|
65
84
|
//# sourceMappingURL=taskfile.js.map
|
|
@@ -13,7 +13,9 @@ export type CompactHookHost = (typeof COMPACT_HOOK_HOSTS)[number];
|
|
|
13
13
|
/** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
|
|
14
14
|
export declare const COMPACT_HOOK_SKIP_HOSTS: readonly ["codex"];
|
|
15
15
|
export type HookVerdict = "allow" | "deny";
|
|
16
|
-
export type HookDecisionCode = "session-start" | "session-start-degraded" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input"
|
|
16
|
+
export type HookDecisionCode = "session-start" | "session-start-degraded" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input"
|
|
17
|
+
/** Host closed stdin with zero bytes — integration failure, not a policy gate (#2864). */
|
|
18
|
+
| "stdin-empty" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready" | "spawn-ready" | "spawn-not-ready" | "runtime-policy-deny-path" | "runtime-policy-deny-scope";
|
|
17
19
|
export interface HookDecision {
|
|
18
20
|
readonly verdict: HookVerdict;
|
|
19
21
|
readonly code: HookDecisionCode;
|
|
@@ -62,6 +64,19 @@ export declare function hookToolName(payload: unknown, host?: HookHost): string
|
|
|
62
64
|
export declare function hookWriteTargetPath(payload: unknown): string | null;
|
|
63
65
|
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
64
66
|
export declare function toProjectRelativePosix(projectRoot: string, targetPath: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Lexical "outside project root" predicate used by #2885.
|
|
69
|
+
* - `".."` / `"../…"` are outside (not bare `startsWith("..")` — that matches `..secret`).
|
|
70
|
+
* - Absolute relatives and win32 cross-drive paths (`D:/…`) are outside.
|
|
71
|
+
* - Drive-letter form is win32-only so POSIX children like `D:/tmp/x` stay in-repo.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isLexicalOutsideProjectRoot(relPosix: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* True when a write target is outside `projectRoot` for the active-scope skip (#2885).
|
|
76
|
+
* Lexically outside paths still fail the skip when a symlink/junction re-enters the project.
|
|
77
|
+
* When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isOutsideProjectRootWrite(projectRoot: string, targetPath: string): boolean;
|
|
65
80
|
/**
|
|
66
81
|
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
67
82
|
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
@@ -81,6 +96,11 @@ export declare function decideHook(input: HookDispatchInput, seams?: HookPolicyS
|
|
|
81
96
|
* hook failure and blocks the tool — so Cursor allows must emit explicit
|
|
82
97
|
* `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
|
|
83
98
|
* flow is unchanged.
|
|
99
|
+
*
|
|
100
|
+
* Cursor stdout always includes `code` (stable machine-readable decision code)
|
|
101
|
+
* so agents can distinguish policy denials from host-integration failures
|
|
102
|
+
* without parsing English (#2864). Exit status still does not encode the
|
|
103
|
+
* verdict — see hook-dispatch `run()` exit-code contract.
|
|
84
104
|
*/
|
|
85
105
|
export declare function renderHostDecision(host: HookHost, decision: HookDecision): string;
|
|
86
106
|
//# sourceMappingURL=dispatcher.d.ts.map
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
3
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
3
4
|
import { evaluateRuntimeAuthorityDirectWrite, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
|
|
4
5
|
import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
|
|
@@ -145,6 +146,57 @@ export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
|
145
146
|
const rel = relative(resolve(projectRoot), abs);
|
|
146
147
|
return rel.split(sep).join("/").replace(/\\/g, "/");
|
|
147
148
|
}
|
|
149
|
+
function posixRelative(fromAbs, toAbs) {
|
|
150
|
+
return relative(fromAbs, toAbs).split(sep).join("/").replace(/\\/g, "/");
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Lexical "outside project root" predicate used by #2885.
|
|
154
|
+
* - `".."` / `"../…"` are outside (not bare `startsWith("..")` — that matches `..secret`).
|
|
155
|
+
* - Absolute relatives and win32 cross-drive paths (`D:/…`) are outside.
|
|
156
|
+
* - Drive-letter form is win32-only so POSIX children like `D:/tmp/x` stay in-repo.
|
|
157
|
+
*/
|
|
158
|
+
export function isLexicalOutsideProjectRoot(relPosix) {
|
|
159
|
+
if (relPosix === ".." || relPosix.startsWith("../") || isAbsolute(relPosix)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
// path.relative returns absolute drive paths across volumes on Windows only.
|
|
163
|
+
return process.platform === "win32" && /^[A-Za-z]:\//.test(relPosix);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* True when a write target is outside `projectRoot` for the active-scope skip (#2885).
|
|
167
|
+
* Lexically outside paths still fail the skip when a symlink/junction re-enters the project.
|
|
168
|
+
* When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
|
|
169
|
+
*/
|
|
170
|
+
export function isOutsideProjectRootWrite(projectRoot, targetPath) {
|
|
171
|
+
const projectAbs = resolve(projectRoot);
|
|
172
|
+
const targetAbs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
|
|
173
|
+
const rel = posixRelative(projectAbs, targetAbs);
|
|
174
|
+
if (!isLexicalOutsideProjectRoot(rel))
|
|
175
|
+
return false;
|
|
176
|
+
try {
|
|
177
|
+
const projectReal = realpathSync(projectAbs);
|
|
178
|
+
let probe = targetAbs;
|
|
179
|
+
for (;;) {
|
|
180
|
+
try {
|
|
181
|
+
const probeReal = realpathSync(probe);
|
|
182
|
+
const reenter = posixRelative(projectReal, probeReal);
|
|
183
|
+
// Empty reenter ⇒ probeReal === projectReal (inside). Lexical-outside reenter ⇒ truly out.
|
|
184
|
+
if (reenter === "" || !isLexicalOutsideProjectRoot(reenter))
|
|
185
|
+
return false;
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
const parent = dirname(probe);
|
|
190
|
+
if (parent === probe)
|
|
191
|
+
return true;
|
|
192
|
+
probe = parent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
148
200
|
/**
|
|
149
201
|
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
150
202
|
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
@@ -298,18 +350,25 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
298
350
|
if (!scope.ready) {
|
|
299
351
|
const writeTarget = hookWriteTargetPath(input.payload);
|
|
300
352
|
const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
353
|
+
// Active-scope governs in-repo lifecycle work only. Outside-root Write/Edit
|
|
354
|
+
// (agent memory, $TMPDIR, user config) skips the deny; null/unparseable
|
|
355
|
+
// targets stay fail-closed. Spawn has no write target → still requires scope (#2885).
|
|
356
|
+
// Lexical ../ + realpath re-entry guard (not bare startsWith(".."); not symlink aliases).
|
|
357
|
+
const outsideRoot = writeTarget !== null && isOutsideProjectRootWrite(projectRoot, writeTarget);
|
|
358
|
+
if (!outsideRoot || isSpawnTool(toolName)) {
|
|
359
|
+
const proposedPathHint = options.proposedLifecycleExempt &&
|
|
360
|
+
relTarget !== null &&
|
|
361
|
+
(relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
|
|
362
|
+
? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
|
|
363
|
+
"filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
|
|
364
|
+
"planning writes (#2625)."
|
|
365
|
+
: " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
|
|
366
|
+
(options.proposedLifecycleExempt
|
|
367
|
+
? "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption)."
|
|
368
|
+
: "then re-run the pre-start_agent gate stack.");
|
|
369
|
+
const denyCode = isSpawnTool(toolName) ? "spawn-not-ready" : "scope-not-ready";
|
|
370
|
+
return deny(input, denyCode, toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
|
|
371
|
+
}
|
|
313
372
|
}
|
|
314
373
|
const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
|
|
315
374
|
if (!isSpawnTool(toolName)) {
|
|
@@ -414,7 +473,12 @@ export function decideHook(input, seams = {}) {
|
|
|
414
473
|
}
|
|
415
474
|
const toolName = hookToolName(input.payload, input.host);
|
|
416
475
|
if (toolName === null) {
|
|
417
|
-
|
|
476
|
+
// stdin-empty is a host-integration failure; keep it distinct from invalid-input
|
|
477
|
+
// so agents can retry without treating it as a policy refusal (#2864).
|
|
478
|
+
const missingCode = input.payloadContext?.stdinEmpty
|
|
479
|
+
? "stdin-empty"
|
|
480
|
+
: "invalid-input";
|
|
481
|
+
return deny(input, missingCode, null, missingToolNameMessage({
|
|
418
482
|
host: input.host,
|
|
419
483
|
payload: input.payload,
|
|
420
484
|
context: input.payloadContext,
|
|
@@ -467,11 +531,16 @@ export function decideHook(input, seams = {}) {
|
|
|
467
531
|
* hook failure and blocks the tool — so Cursor allows must emit explicit
|
|
468
532
|
* `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
|
|
469
533
|
* flow is unchanged.
|
|
534
|
+
*
|
|
535
|
+
* Cursor stdout always includes `code` (stable machine-readable decision code)
|
|
536
|
+
* so agents can distinguish policy denials from host-integration failures
|
|
537
|
+
* without parsing English (#2864). Exit status still does not encode the
|
|
538
|
+
* verdict — see hook-dispatch `run()` exit-code contract.
|
|
470
539
|
*/
|
|
471
540
|
export function renderHostDecision(host, decision) {
|
|
472
541
|
if (decision.verdict === "allow") {
|
|
473
542
|
if (host === "cursor") {
|
|
474
|
-
return JSON.stringify({ permission: "allow" });
|
|
543
|
+
return JSON.stringify({ permission: "allow", code: decision.code });
|
|
475
544
|
}
|
|
476
545
|
return "";
|
|
477
546
|
}
|
|
@@ -492,6 +561,7 @@ export function renderHostDecision(host, decision) {
|
|
|
492
561
|
permission: "deny",
|
|
493
562
|
user_message: decision.message,
|
|
494
563
|
agent_message: decision.message,
|
|
564
|
+
code: decision.code,
|
|
495
565
|
});
|
|
496
566
|
}
|
|
497
567
|
}
|
|
@@ -1,11 +1,51 @@
|
|
|
1
1
|
import type { ScmCallFn } from "./reconcile-issues.js";
|
|
2
2
|
export declare const GITHUB_ISSUE_REF_TYPE = "x-xbrief/github-issue";
|
|
3
3
|
export declare const EXTERNAL_TRUST_LEVEL = "external";
|
|
4
|
+
/**
|
|
5
|
+
* Structured post-create failure: remote issue exists; local stamp/ledger may not.
|
|
6
|
+
* `createdUrl` is the durable handle for retry (also mirrored in process + OS-temp recovery).
|
|
7
|
+
*/
|
|
4
8
|
export declare class IssueEmitError extends Error {
|
|
5
|
-
|
|
9
|
+
readonly createdUrl?: string;
|
|
10
|
+
constructor(message: string, options?: {
|
|
11
|
+
createdUrl?: string;
|
|
12
|
+
});
|
|
6
13
|
}
|
|
14
|
+
export declare function recoverySidecarPath(vbriefAbsPath: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Always-on recovery after remote create: process memory + private OS-temp sidecar.
|
|
17
|
+
* Independent of project-contained ledger so dual local failure still reconciles on retry (#2880).
|
|
18
|
+
*/
|
|
19
|
+
export declare function rememberCreatedUrl(vbriefAbsPath: string, url: string): void;
|
|
20
|
+
export declare function loadRecoveredUrl(vbriefAbsPath: string): string | undefined;
|
|
21
|
+
export declare function clearRecoveredUrl(vbriefAbsPath: string): void;
|
|
22
|
+
/** Project ledger first, then process/OS-temp recovery (#2880). */
|
|
23
|
+
export declare function resolvePriorCreatedUrl(projectRoot: string, vbriefAbsPath: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Record URL immediately after remote create. Project ledger is best-effort;
|
|
26
|
+
* recovery layers always run first so dual local failure cannot force re-create (#2880).
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordCreatedUrlDurable(projectRoot: string, vbriefAbsPath: string, url: string): void;
|
|
29
|
+
/** Contained durable map: abs vbrief path -> issue URL for in-flight emits (#2871). */
|
|
30
|
+
export declare function pendingEmitLedgerPath(projectRoot: string): string;
|
|
31
|
+
export declare function loadPendingEmitUrls(projectRoot: string): Record<string, string>;
|
|
32
|
+
export declare function savePendingEmitUrl(projectRoot: string, vbriefAbsPath: string, url: string): void;
|
|
33
|
+
export declare function clearPendingEmitUrl(projectRoot: string, vbriefAbsPath: string): void;
|
|
7
34
|
export declare function loadVbrief(path: string): Record<string, unknown>;
|
|
8
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Resolve project root and refuse unsafe write targets before any emit side-effect.
|
|
37
|
+
* Callers that create remote issues MUST invoke this before `fileIssue` so a
|
|
38
|
+
* containment refusal cannot leave an orphan GitHub issue (#2869 / #2871).
|
|
39
|
+
*
|
|
40
|
+
* ⊗ Do not fall back to `dirname(path)` as the containment root — that trusts a
|
|
41
|
+
* possibly-symlinked parent and would re-open the escape (Greptile P1 on #2871).
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertVbriefWriteTargetSafe(path: string, projectRoot?: string | null): string;
|
|
44
|
+
/**
|
|
45
|
+
* Persist an xBRIEF/vBRIEF JSON document. Gates the write target so a leaf or
|
|
46
|
+
* parent-directory symlink cannot divert the stamped file outside the project (#2869).
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeVbrief(path: string, data: Record<string, unknown>, projectRoot?: string | null): void;
|
|
9
49
|
export declare function vbriefTitle(data: Record<string, unknown>): string;
|
|
10
50
|
export declare function existingGithubIssueRef(data: Record<string, unknown>): string | null | undefined;
|
|
11
51
|
export declare function addGithubIssueReference(data: Record<string, unknown>, url: string): Record<string, unknown>;
|
|
@@ -23,12 +63,14 @@ export declare function emitSingle(path: string, options: {
|
|
|
23
63
|
scmCall?: ScmCallFn;
|
|
24
64
|
noNetwork?: boolean;
|
|
25
65
|
displayPath?: string | null;
|
|
66
|
+
projectRoot?: string | null;
|
|
26
67
|
}): EmitAction;
|
|
27
68
|
export declare function emitPerVbrief(paths: string[], options: {
|
|
28
69
|
repo: string;
|
|
29
70
|
scmCall?: ScmCallFn;
|
|
30
71
|
noNetwork?: boolean;
|
|
31
72
|
displayPaths?: string[] | null;
|
|
73
|
+
projectRoot?: string | null;
|
|
32
74
|
}): EmitAction[];
|
|
33
75
|
export interface UmbrellaAction {
|
|
34
76
|
result: "created" | "dryrun" | "skipped";
|
|
@@ -45,6 +87,7 @@ export declare function emitUmbrella(paths: string[], options: {
|
|
|
45
87
|
noNetwork?: boolean;
|
|
46
88
|
title?: string | null;
|
|
47
89
|
displayPaths?: string[] | null;
|
|
90
|
+
projectRoot?: string | null;
|
|
48
91
|
}): UmbrellaAction;
|
|
49
92
|
export declare function expandPatterns(patterns: string[], root?: string | null): string[];
|
|
50
93
|
export declare function isNoNetwork(dryRun: boolean): boolean;
|