@deftai/directive-core 0.93.0 → 0.94.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/fetch.d.ts +29 -0
- package/dist/cache/fetch.js +131 -4
- package/dist/cache/operations.d.ts +8 -0
- package/dist/cache/operations.js +67 -4
- package/dist/check/cached-orchestrator.js +11 -0
- package/dist/check/consumer-gate-integrity.d.ts +68 -0
- package/dist/check/consumer-gate-integrity.js +265 -0
- package/dist/check/index.d.ts +1 -0
- package/dist/check/index.js +1 -0
- package/dist/check/orchestrator.js +10 -0
- package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
- package/dist/content-contracts/skills/greptile-detector.js +9 -2
- package/dist/doctor/main.js +57 -0
- package/dist/finish-loop/pr-finish-loop.js +1 -0
- package/dist/hooks/dispatcher.d.ts +24 -2
- package/dist/hooks/dispatcher.js +122 -11
- package/dist/hooks/readonly.d.ts +14 -0
- package/dist/hooks/readonly.js +126 -0
- package/dist/intake/issue-ingest.d.ts +20 -0
- package/dist/intake/issue-ingest.js +58 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/policy/min-greptile-confidence.d.ts +57 -0
- package/dist/policy/min-greptile-confidence.js +123 -0
- package/dist/pr-merge-readiness/compute.d.ts +11 -1
- package/dist/pr-merge-readiness/compute.js +17 -4
- package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
- package/dist/pr-merge-readiness/evaluate.js +7 -4
- package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
- package/dist/pr-merge-readiness/mergeability.js +14 -2
- package/dist/pr-monitor/main.d.ts +2 -0
- package/dist/pr-monitor/main.js +31 -1
- package/dist/pr-monitor/monitor.js +5 -1
- package/dist/pr-monitor/readiness.js +3 -1
- package/dist/pr-monitor/types.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -1
- package/dist/pr-wait-mergeable/main.d.ts +2 -0
- package/dist/pr-wait-mergeable/main.js +16 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +3 -0
- package/dist/pr-watch/constants.d.ts +4 -1
- package/dist/pr-watch/constants.js +5 -2
- package/dist/pr-watch/main.js +1 -0
- package/dist/pr-watch/probe.d.ts +1 -1
- package/dist/pr-watch/probe.js +4 -1
- package/dist/pr-watch/types.d.ts +7 -1
- package/dist/pr-watch/watch.js +3 -2
- package/dist/review-monitor/index.d.ts +1 -0
- package/dist/review-monitor/index.js +1 -0
- package/dist/review-monitor/l4-owner.d.ts +46 -0
- package/dist/review-monitor/l4-owner.js +262 -0
- package/dist/tool-events/classify.js +1 -0
- package/dist/triage/welcome/writers.js +16 -7
- package/dist/umbrella-current-shape/index.d.ts +45 -0
- package/dist/umbrella-current-shape/index.js +162 -8
- package/dist/vbrief-reconcile/index.d.ts +2 -2
- package/dist/vbrief-reconcile/index.js +1 -1
- package/dist/vbrief-reconcile/types.d.ts +14 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
- package/dist/vbrief-reconcile/umbrellas.js +231 -19
- package/package.json +3 -3
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer check-graph integrity (#3070).
|
|
3
|
+
*
|
|
4
|
+
* CONSUMER_CHECK_GATES lists Taskfile tasks that must resolve in a vendored
|
|
5
|
+
* deposit. When optional Taskfile includes silently omit `tasks/verify.yml`
|
|
6
|
+
* (etc.), go-task fails with opaque exit 200/201 ("Task does not exist").
|
|
7
|
+
*
|
|
8
|
+
* This module:
|
|
9
|
+
* 1. Maps each gate to its include namespace / root Taskfile surface
|
|
10
|
+
* 2. Proves shipped files define those tasks (static + runtime)
|
|
11
|
+
* 3. Emits a deposit-repair recovery message instead of opaque go-task errors
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { CONSUMER_CHECK_GATES, checkGateId } from "./gate-lists.js";
|
|
16
|
+
/** Include namespaces required by the consumer check graph (Taskfile includes). */
|
|
17
|
+
export const CHECK_GRAPH_REQUIRED_NAMESPACES = ["verify", "toolchain", "vbrief"];
|
|
18
|
+
export const CONSUMER_GATE_INTEGRITY_RECOVERY = "Incomplete Deft deposit: check-graph Taskfile include(s) missing or incomplete. " +
|
|
19
|
+
"Run `deft update` (or `npm i -g @deftai/directive@latest` then `deft update`) " +
|
|
20
|
+
"to restore `.deft/core/tasks/` (including `tasks/verify.yml` for `verify:orphan-active`). " +
|
|
21
|
+
"See UPGRADING.md.";
|
|
22
|
+
/** Namespace for a namespaced task (`verify:orphan-active` → `verify`); null for root tasks. */
|
|
23
|
+
export function gateNamespace(gateId) {
|
|
24
|
+
const colon = gateId.indexOf(":");
|
|
25
|
+
if (colon <= 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return gateId.slice(0, colon);
|
|
29
|
+
}
|
|
30
|
+
/** Local task name inside an include (`verify:orphan-active` → `orphan-active`). */
|
|
31
|
+
export function gateLocalName(gateId) {
|
|
32
|
+
const colon = gateId.indexOf(":");
|
|
33
|
+
if (colon <= 0) {
|
|
34
|
+
return gateId;
|
|
35
|
+
}
|
|
36
|
+
return gateId.slice(colon + 1);
|
|
37
|
+
}
|
|
38
|
+
/** Relative path of the include Taskfile for a namespace. */
|
|
39
|
+
export function includeTaskfileRel(namespace) {
|
|
40
|
+
return `tasks/${namespace}.yml`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Namespaces that CONSUMER_CHECK_GATES require via `ns:task` form.
|
|
44
|
+
* Root-only gates (doctor, verify-strategy-output) do not add a namespace.
|
|
45
|
+
*/
|
|
46
|
+
export function requiredNamespacesForGates(gates = CONSUMER_CHECK_GATES) {
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const spec of gates) {
|
|
49
|
+
const ns = gateNamespace(checkGateId(spec));
|
|
50
|
+
if (ns !== null) {
|
|
51
|
+
seen.add(ns);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...seen].sort();
|
|
55
|
+
}
|
|
56
|
+
/** True when `localName` is a top-level task key under a Taskfile `tasks:` section. */
|
|
57
|
+
export function taskDefinedInTaskfileYaml(text, localName) {
|
|
58
|
+
// go-task task keys are indented two spaces under `tasks:`.
|
|
59
|
+
// Match ` orphan-active:` / ` check-consumer:` etc., not nested keys.
|
|
60
|
+
const escaped = localName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
const re = new RegExp(`^ {2}${escaped}\\s*:`, "m");
|
|
62
|
+
return re.test(text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse root Taskfile `includes:` entries: map namespace → { taskfile, optional }.
|
|
66
|
+
* Minimal line parser (avoids a YAML dependency); sufficient for go-task include shape.
|
|
67
|
+
*/
|
|
68
|
+
export function parseTaskfileIncludes(text) {
|
|
69
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
70
|
+
const result = new Map();
|
|
71
|
+
let inIncludes = false;
|
|
72
|
+
let includesIndent = 0;
|
|
73
|
+
let currentNs = null;
|
|
74
|
+
let currentTaskfile = null;
|
|
75
|
+
let currentOptional = true; // go-task default when omitted is false, but our deposit historically used true
|
|
76
|
+
const flush = () => {
|
|
77
|
+
if (currentNs !== null && currentTaskfile !== null) {
|
|
78
|
+
result.set(currentNs, { taskfile: currentTaskfile, optional: currentOptional });
|
|
79
|
+
}
|
|
80
|
+
currentNs = null;
|
|
81
|
+
currentTaskfile = null;
|
|
82
|
+
currentOptional = false;
|
|
83
|
+
};
|
|
84
|
+
for (const raw of lines) {
|
|
85
|
+
const stripped = raw.trim();
|
|
86
|
+
if (!stripped || stripped.startsWith("#")) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const indent = raw.length - raw.trimStart().length;
|
|
90
|
+
if (!inIncludes) {
|
|
91
|
+
if (/^includes\s*:/.test(stripped) && indent === 0) {
|
|
92
|
+
inIncludes = true;
|
|
93
|
+
includesIndent = indent;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (indent <= includesIndent && !stripped.startsWith("#")) {
|
|
98
|
+
flush();
|
|
99
|
+
inIncludes = false;
|
|
100
|
+
// fall through if a new top-level key; re-check next iteration naturally
|
|
101
|
+
if (/^includes\s*:/.test(stripped)) {
|
|
102
|
+
inIncludes = true;
|
|
103
|
+
includesIndent = indent;
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Namespace key at includes+2 (typically 2 spaces): ` verify:`
|
|
108
|
+
if (indent === includesIndent + 2 && /^[A-Za-z_][\w-]*\s*:/.test(stripped)) {
|
|
109
|
+
flush();
|
|
110
|
+
currentNs = stripped.replace(/:\s*(?:#.*)?$/, "").trim();
|
|
111
|
+
currentTaskfile = null;
|
|
112
|
+
currentOptional = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (currentNs === null) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Properties at includes+4: taskfile / optional
|
|
119
|
+
if (indent >= includesIndent + 4) {
|
|
120
|
+
const taskfileMatch = stripped.match(/^taskfile\s*:\s*["']?([^"'#]+?)["']?\s*(?:#.*)?$/i);
|
|
121
|
+
if (taskfileMatch?.[1]) {
|
|
122
|
+
currentTaskfile = taskfileMatch[1].trim();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const optionalMatch = stripped.match(/^optional\s*:\s*(true|false)\s*(?:#.*)?$/i);
|
|
126
|
+
if (optionalMatch?.[1]) {
|
|
127
|
+
currentOptional = optionalMatch[1].toLowerCase() === "true";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
flush();
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Assert every consumer check gate resolves against `frameworkRoot` Taskfile + includes.
|
|
136
|
+
* When the root Taskfile is absent, returns ok with no findings (caller/spawn handles that).
|
|
137
|
+
*/
|
|
138
|
+
export function evaluateConsumerGateIntegrity(frameworkRoot, seams = {}) {
|
|
139
|
+
const root = resolve(frameworkRoot);
|
|
140
|
+
const exists = seams.exists ?? ((p) => existsSync(p));
|
|
141
|
+
const readText = seams.readText ??
|
|
142
|
+
((p) => {
|
|
143
|
+
try {
|
|
144
|
+
if (!existsSync(p)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return readFileSync(p, "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const gates = seams.gates ?? CONSUMER_CHECK_GATES;
|
|
154
|
+
const findings = [];
|
|
155
|
+
const rootTaskfile = join(root, "Taskfile.yml");
|
|
156
|
+
if (!exists(rootTaskfile)) {
|
|
157
|
+
// Incomplete / non-deposit path: do not invent a Taskfile; let spawn fail as before.
|
|
158
|
+
return { ok: true, findings: [], recovery: CONSUMER_GATE_INTEGRITY_RECOVERY };
|
|
159
|
+
}
|
|
160
|
+
const rootText = readText(rootTaskfile);
|
|
161
|
+
if (rootText === null) {
|
|
162
|
+
findings.push({
|
|
163
|
+
gateId: "*",
|
|
164
|
+
kind: "missing-root-taskfile",
|
|
165
|
+
detail: `Taskfile.yml unreadable at ${rootTaskfile}`,
|
|
166
|
+
expectedPath: rootTaskfile,
|
|
167
|
+
});
|
|
168
|
+
return { ok: false, findings, recovery: CONSUMER_GATE_INTEGRITY_RECOVERY };
|
|
169
|
+
}
|
|
170
|
+
const includes = parseTaskfileIncludes(rootText);
|
|
171
|
+
const yamlCache = new Map();
|
|
172
|
+
const readCached = (abs) => {
|
|
173
|
+
if (yamlCache.has(abs)) {
|
|
174
|
+
return yamlCache.get(abs) ?? null;
|
|
175
|
+
}
|
|
176
|
+
const t = readText(abs);
|
|
177
|
+
yamlCache.set(abs, t);
|
|
178
|
+
return t;
|
|
179
|
+
};
|
|
180
|
+
yamlCache.set(rootTaskfile, rootText);
|
|
181
|
+
for (const spec of gates) {
|
|
182
|
+
const gateId = checkGateId(spec);
|
|
183
|
+
const ns = gateNamespace(gateId);
|
|
184
|
+
const local = gateLocalName(gateId);
|
|
185
|
+
if (ns === null) {
|
|
186
|
+
// Root Taskfile task (doctor, verify-strategy-output, …)
|
|
187
|
+
if (!taskDefinedInTaskfileYaml(rootText, local)) {
|
|
188
|
+
findings.push({
|
|
189
|
+
gateId,
|
|
190
|
+
kind: "missing-task-definition",
|
|
191
|
+
detail: `Root task "${local}" not defined in Taskfile.yml (required by consumer check gate "${gateId}")`,
|
|
192
|
+
expectedPath: rootTaskfile,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const rel = includeTaskfileRel(ns);
|
|
198
|
+
const includeMeta = includes.get(ns);
|
|
199
|
+
const relFromInclude = includeMeta?.taskfile ? includeMeta.taskfile.replace(/^\.\//, "") : rel;
|
|
200
|
+
const includeAbs = join(root, relFromInclude);
|
|
201
|
+
if (!exists(includeAbs)) {
|
|
202
|
+
findings.push({
|
|
203
|
+
gateId,
|
|
204
|
+
kind: "missing-include-file",
|
|
205
|
+
detail: `Missing Taskfile include file for namespace "${ns}" (gate "${gateId}"): ` +
|
|
206
|
+
`${relFromInclude}. Optional silent omit of this include causes go-task ` +
|
|
207
|
+
`"Task \\"${gateId}\\" does not exist" (exit 200/201).`,
|
|
208
|
+
expectedPath: includeAbs,
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const includeText = readCached(includeAbs);
|
|
213
|
+
if (includeText === null) {
|
|
214
|
+
findings.push({
|
|
215
|
+
gateId,
|
|
216
|
+
kind: "missing-include-file",
|
|
217
|
+
detail: `Unreadable Taskfile include for namespace "${ns}": ${relFromInclude}`,
|
|
218
|
+
expectedPath: includeAbs,
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!taskDefinedInTaskfileYaml(includeText, local)) {
|
|
223
|
+
findings.push({
|
|
224
|
+
gateId,
|
|
225
|
+
kind: "missing-task-definition",
|
|
226
|
+
detail: `Task "${local}" not defined in ${relFromInclude} (required by consumer check gate "${gateId}")`,
|
|
227
|
+
expectedPath: includeAbs,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
ok: findings.length === 0,
|
|
233
|
+
findings,
|
|
234
|
+
recovery: CONSUMER_GATE_INTEGRITY_RECOVERY,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/** Format integrity failure for stderr (check orchestrator / doctor). */
|
|
238
|
+
export function formatConsumerGateIntegrityFailure(result) {
|
|
239
|
+
const lines = [
|
|
240
|
+
"check: consumer gate integrity failed (#3070)",
|
|
241
|
+
...result.findings.map((f) => ` - ${f.gateId}: ${f.detail}`),
|
|
242
|
+
` recovery: ${result.recovery}`,
|
|
243
|
+
];
|
|
244
|
+
return `${lines.join("\n")}\n`;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Namespaces that must not be `optional: true` on the root Taskfile when the
|
|
248
|
+
* check graph depends on them — silent omit is the #3070 failure mode.
|
|
249
|
+
*/
|
|
250
|
+
export function checkGraphOptionalIncludeViolations(rootTaskfileText, namespaces = requiredNamespacesForGates()) {
|
|
251
|
+
const includes = parseTaskfileIncludes(rootTaskfileText);
|
|
252
|
+
const bad = [];
|
|
253
|
+
for (const ns of namespaces) {
|
|
254
|
+
const meta = includes.get(ns);
|
|
255
|
+
if (meta === undefined) {
|
|
256
|
+
bad.push(`${ns} (include entry missing)`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (meta.optional) {
|
|
260
|
+
bad.push(`${ns} (optional: true — check-graph includes must fail loud when file is missing)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return bad;
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=consumer-gate-integrity.js.map
|
package/dist/check/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
|
+
export { CHECK_GRAPH_REQUIRED_NAMESPACES, CONSUMER_GATE_INTEGRITY_RECOVERY, type ConsumerGateIntegrityFinding, type ConsumerGateIntegrityResult, type ConsumerGateIntegritySeams, checkGraphOptionalIncludeViolations, evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, gateLocalName, gateNamespace, includeTaskfileRel, parseTaskfileIncludes, requiredNamespacesForGates, taskDefinedInTaskfileYaml, } from "./consumer-gate-integrity.js";
|
|
2
3
|
export { type CheckGateSpec, CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
3
4
|
export type { CheckOrchestratorOptions, CheckOrchestratorSeams } from "./orchestrator.js";
|
|
4
5
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
package/dist/check/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
2
|
+
export { CHECK_GRAPH_REQUIRED_NAMESPACES, CONSUMER_GATE_INTEGRITY_RECOVERY, checkGraphOptionalIncludeViolations, evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, gateLocalName, gateNamespace, includeTaskfileRel, parseTaskfileIncludes, requiredNamespacesForGates, taskDefinedInTaskfileYaml, } from "./consumer-gate-integrity.js";
|
|
2
3
|
export { CONSUMER_CHECK_GATES, checkGateId, checkGateSpawnArgs, FRAMEWORK_CHECK_GATES, gatesForCheckTarget, } from "./gate-lists.js";
|
|
3
4
|
export { dispatchTaskCheck, isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget, } from "./orchestrator.js";
|
|
4
5
|
export { detectTestRunner, runnerDetectionTable, } from "./runner-detect.js";
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
16
|
import { dispatchCachedTaskCheck } from "./cached-orchestrator.js";
|
|
17
|
+
import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "./consumer-gate-integrity.js";
|
|
17
18
|
import { resolveCheckTarget } from "./context.js";
|
|
18
19
|
export { isFrameworkRepoRoot, isFrameworkSourceContext, resolveCheckTarget } from "./context.js";
|
|
19
20
|
/**
|
|
@@ -35,6 +36,15 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
|
|
|
35
36
|
const taskBin = seams.taskBin ?? "task";
|
|
36
37
|
const target = resolveCheckTarget(resolvedFramework, resolvedProject);
|
|
37
38
|
const cwd = target === "check:framework-source" ? resolvedFramework : resolvedProject;
|
|
39
|
+
// #3070: pre-flight consumer check-graph integrity (same path as cached
|
|
40
|
+
// orchestrator) so uncached aggregate shelling also fails with recovery text.
|
|
41
|
+
if (target === "check:consumer") {
|
|
42
|
+
const integrity = evaluateConsumerGateIntegrity(resolvedFramework);
|
|
43
|
+
if (!integrity.ok) {
|
|
44
|
+
process.stderr.write(formatConsumerGateIntegrityFailure(integrity));
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
const spawn = seams.spawnFn ?? defaultSpawn;
|
|
39
49
|
const result = spawn(taskBin, [target, "--taskfile", taskfilePath], {
|
|
40
50
|
cwd,
|
|
@@ -34,6 +34,13 @@ export declare const BODY_AC4_INLINE_SHA_CLEAN: string;
|
|
|
34
34
|
export declare const BODY_AC4_THIRD_CONFIDENCE_FORM: string;
|
|
35
35
|
export declare const BODY_AC4_EMPTY = "";
|
|
36
36
|
export declare const BODY_AC4_TRUNCATED: string;
|
|
37
|
+
/**
|
|
38
|
+
* Fail-closed CLEAN gate shared by pr:watch, swarm poller, and content-contracts.
|
|
39
|
+
*
|
|
40
|
+
* `minConfidence` defaults to the consumer bar (4 == legacy confidence > 3).
|
|
41
|
+
* Directive dogfood and project policy resolve a higher floor via
|
|
42
|
+
* `resolveMinGreptileConfidence` (#3095).
|
|
43
|
+
*/
|
|
37
44
|
export declare function evaluateCleanGate(params: {
|
|
38
45
|
lastReviewedSha: string | null;
|
|
39
46
|
headSha: string;
|
|
@@ -42,6 +49,8 @@ export declare function evaluateCleanGate(params: {
|
|
|
42
49
|
ciFailures: number;
|
|
43
50
|
errored: boolean;
|
|
44
51
|
terminalCheckRun?: boolean;
|
|
52
|
+
/** Minimum confidence score (1–5) that CLEANs; score must be >= min. Default 4. */
|
|
53
|
+
minConfidence?: number;
|
|
45
54
|
}): [boolean, string | null];
|
|
46
55
|
type PollExitClass = "CLEAN" | "NEW_P0P1" | "ERRORED" | "STALL" | "RUNNING";
|
|
47
56
|
export declare function simulatePollLoop(params: {
|
|
@@ -277,15 +277,22 @@ export const BODY_AC4_THIRD_CONFIDENCE_FORM = "Greptile review of head 1234567\n
|
|
|
277
277
|
`Last reviewed commit: [fix: foo](https://github.com/deftai/directive/commit/${HEAD_SHA})\n`;
|
|
278
278
|
export const BODY_AC4_EMPTY = "";
|
|
279
279
|
export const BODY_AC4_TRUNCATED = "Greptile review of head 1234567\n" + "\n" + "## Confidence Score:";
|
|
280
|
+
/**
|
|
281
|
+
* Fail-closed CLEAN gate shared by pr:watch, swarm poller, and content-contracts.
|
|
282
|
+
*
|
|
283
|
+
* `minConfidence` defaults to the consumer bar (4 == legacy confidence > 3).
|
|
284
|
+
* Directive dogfood and project policy resolve a higher floor via
|
|
285
|
+
* `resolveMinGreptileConfidence` (#3095).
|
|
286
|
+
*/
|
|
280
287
|
export function evaluateCleanGate(params) {
|
|
281
|
-
const { lastReviewedSha, headSha, hasBlocking, confidence, ciFailures, errored, terminalCheckRun = true, } = params;
|
|
288
|
+
const { lastReviewedSha, headSha, hasBlocking, confidence, ciFailures, errored, terminalCheckRun = true, minConfidence = 4, } = params;
|
|
282
289
|
if (lastReviewedSha === null || lastReviewedSha !== headSha) {
|
|
283
290
|
return [false, "sha_match"];
|
|
284
291
|
}
|
|
285
292
|
if (hasBlocking) {
|
|
286
293
|
return [false, "has_blocking"];
|
|
287
294
|
}
|
|
288
|
-
if (confidence === null || confidence
|
|
295
|
+
if (confidence === null || confidence < minConfidence) {
|
|
289
296
|
return [false, "confidence"];
|
|
290
297
|
}
|
|
291
298
|
if (ciFailures > 0) {
|
package/dist/doctor/main.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { VBRIEF_VERSION } from "@deftai/directive-types";
|
|
4
4
|
import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
|
|
5
|
+
import { evaluateConsumerGateIntegrity, formatConsumerGateIntegrityFailure, } from "../check/consumer-gate-integrity.js";
|
|
5
6
|
import { contentRoot } from "../content-root.js";
|
|
6
7
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
7
8
|
import { DEFT_DIRECTIVE_DISABLE_FLAG_NAME, DEFT_DIRECTIVE_DISABLE_STATUS, DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING, detectDeftDirectiveDisable, formatDeftDirectiveDisableMessage, isDeftDirectiveDisableActive, } from "../policy/deft-directive-disable.js";
|
|
@@ -389,6 +390,11 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
389
390
|
if (!jsonMode) {
|
|
390
391
|
sink.blank();
|
|
391
392
|
}
|
|
393
|
+
sink.info("Checking consumer check-graph integrity (verify:orphan-active and peers)...");
|
|
394
|
+
runConsumerCheckGraphIntegrity(projectRoot, frameworkRoot, sink, addFinding, seams);
|
|
395
|
+
if (!jsonMode) {
|
|
396
|
+
sink.blank();
|
|
397
|
+
}
|
|
392
398
|
sink.info("Checking OpenClaw always-pin skills...");
|
|
393
399
|
runOpenClawSkillPinsCheck(sink, addFinding, {
|
|
394
400
|
frameworkRoot,
|
|
@@ -763,6 +769,57 @@ function runAgentsMdAdvisoryCheck(projectRoot, sink, addFinding, seams) {
|
|
|
763
769
|
addFinding({ severity: "warning", message, check: checkName });
|
|
764
770
|
}
|
|
765
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Consumer check-graph integrity (#3070): every CONSUMER_CHECK_GATES entry
|
|
774
|
+
* (including `verify:orphan-active`) must resolve against the deposited
|
|
775
|
+
* Taskfile includes. Missing `tasks/verify.yml` previously failed only at
|
|
776
|
+
* go-task shell time with opaque exit 200/201.
|
|
777
|
+
*/
|
|
778
|
+
function runConsumerCheckGraphIntegrity(projectRoot, frameworkRoot, sink, addFinding, seams) {
|
|
779
|
+
const checkName = "consumer-check-graph-integrity";
|
|
780
|
+
if (runningInsideDeftRepo(projectRoot, seams)) {
|
|
781
|
+
// Still prove the source checkout ships a resolvable consumer gate set.
|
|
782
|
+
const integrity = evaluateConsumerGateIntegrity(frameworkRoot);
|
|
783
|
+
if (integrity.ok) {
|
|
784
|
+
sink.success(`${checkName}: source checkout resolves CONSUMER_CHECK_GATES (incl. verify:orphan-active)`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const message = formatConsumerGateIntegrityFailure(integrity).trim();
|
|
788
|
+
sink.error(message);
|
|
789
|
+
addFinding({
|
|
790
|
+
severity: "error",
|
|
791
|
+
message,
|
|
792
|
+
check: checkName,
|
|
793
|
+
suggestion: "deft update",
|
|
794
|
+
status: "fail",
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
const depositRoot = frameworkRoot;
|
|
800
|
+
const integrity = evaluateConsumerGateIntegrity(depositRoot);
|
|
801
|
+
if (integrity.ok) {
|
|
802
|
+
sink.success(`${checkName}: deposit resolves CONSUMER_CHECK_GATES (incl. verify:orphan-active)`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Incomplete deposit is an error for consumers — check:consumer would hard-fail.
|
|
806
|
+
const message = formatConsumerGateIntegrityFailure(integrity).trim();
|
|
807
|
+
sink.error(message);
|
|
808
|
+
addFinding({
|
|
809
|
+
severity: "error",
|
|
810
|
+
message,
|
|
811
|
+
check: checkName,
|
|
812
|
+
suggestion: integrity.recovery,
|
|
813
|
+
status: "fail",
|
|
814
|
+
findings: integrity.findings,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (exc) {
|
|
818
|
+
const message = `${checkName}: probe failed -- ${exc instanceof Error ? exc.name : "Error"}: ${exc}`;
|
|
819
|
+
sink.warn(message);
|
|
820
|
+
addFinding({ severity: "warning", message, check: checkName });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
766
823
|
function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams) {
|
|
767
824
|
if (runningInsideDeftRepo(projectRoot, seams)) {
|
|
768
825
|
sink.info("Skipping Taskfile include check -- running inside the deft framework repo (the repo's own Taskfile.yml is the surface).");
|
|
@@ -5,7 +5,7 @@ import { type RuntimeAuthorityPolicy } from "../policy/runtime-authority.js";
|
|
|
5
5
|
import { type VerifyResult } from "../session/verify-session-ritual.js";
|
|
6
6
|
import { type HookPayloadContext } from "./classify/index.js";
|
|
7
7
|
import { type ActiveScopeInspection } from "./scope.js";
|
|
8
|
-
export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
8
|
+
export { ASSIST_SESSION_POSTURE_ENV, hookReadOnlyFromPayload, isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
|
|
9
9
|
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
10
10
|
export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
|
|
11
11
|
export type HookHost = (typeof HOOK_HOSTS)[number];
|
|
@@ -21,7 +21,11 @@ export type HookDecisionCode = "session-start" | "session-start-disabled" | "ses
|
|
|
21
21
|
/** Enforcement skipped: root `.deft-directive-disable` test kill-switch (#3039). */
|
|
22
22
|
| "directive-disabled" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input"
|
|
23
23
|
/** Host closed stdin with zero bytes — integration failure, not a policy gate (#2864). */
|
|
24
|
-
| "stdin-empty" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready"
|
|
24
|
+
| "stdin-empty" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready"
|
|
25
|
+
/** Allowlisted assist/scratch write without active xBRIEF (#1802). */
|
|
26
|
+
| "write-assist-scratch-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready"
|
|
27
|
+
/** Non-lifecycle assist/docs spawn allowed without active xBRIEF (#3080). */
|
|
28
|
+
| "spawn-ephemeral-ready" | "spawn-ready" | "spawn-not-ready" | "runtime-policy-deny-path" | "runtime-policy-deny-scope"
|
|
25
29
|
/** Shell/MCP classifiable push/merge allowed under runtimeAuthority (#2711). */
|
|
26
30
|
| "shell-op-ready"
|
|
27
31
|
/** Shell/MCP tool seen but command/tool not classifiable as push/merge — fail open (#2711). */
|
|
@@ -102,6 +106,24 @@ export declare function isOutsideProjectRootWrite(projectRoot: string, targetPat
|
|
|
102
106
|
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
103
107
|
*/
|
|
104
108
|
export declare function isProposedLifecycleWrite(projectRoot: string, targetPath: string | null): boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Canonical + gitignored assist scratch roots for low-ceremony disposable notes (#1802).
|
|
111
|
+
* Path fence only — not free-text "for Obsidian" NLP. Tracked trees (src/, packages/, …)
|
|
112
|
+
* are never listed here.
|
|
113
|
+
*/
|
|
114
|
+
export declare const ASSIST_SCRATCH_ROOT_PREFIXES: readonly [".deft-scratch/", "temp/"];
|
|
115
|
+
/**
|
|
116
|
+
* True when the write target is under an allowlisted disposable scratch root (#1802).
|
|
117
|
+
* Fail closed on null/empty/unparseable targets and on path escape (`..`).
|
|
118
|
+
* Does not authorize tracked product paths even under assist posture.
|
|
119
|
+
*/
|
|
120
|
+
export declare function isAllowlistedAssistScratchPath(projectRoot: string, targetPath: string | null): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Assist/ephemeral classification for scratch-write carve-out (#1802).
|
|
123
|
+
* Requires allowlisted path AND (assist posture markers OR ephemeral spawn markers).
|
|
124
|
+
* Path alone without posture markers fails closed to the mutation gate stack.
|
|
125
|
+
*/
|
|
126
|
+
export declare function isAssistScratchWrite(projectRoot: string, targetPath: string | null, payload: unknown, environ?: NodeJS.ProcessEnv): boolean;
|
|
105
127
|
/** Normalize hook project-root resolution on Windows (doubled drive + MSYS `/c/...`). */
|
|
106
128
|
export declare function normalizeHookProjectRoot(path: string): string;
|
|
107
129
|
export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -13,12 +13,12 @@ import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
|
|
|
13
13
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
14
14
|
import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
|
|
15
15
|
import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
|
|
16
|
-
import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
16
|
+
import { isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
|
|
17
17
|
import { inspectActiveScope } from "./scope.js";
|
|
18
18
|
import { isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool } from "./tools.js";
|
|
19
19
|
// Pure parse/classify helpers are defined in ./classify/ and re-exported from
|
|
20
20
|
// ./index.ts (#2950). Dispatcher is orchestration: classify → policy → decision.
|
|
21
|
-
export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
21
|
+
export { ASSIST_SESSION_POSTURE_ENV, hookReadOnlyFromPayload, isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
|
|
22
22
|
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
23
23
|
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
24
24
|
export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
|
|
@@ -99,6 +99,47 @@ export function isProposedLifecycleWrite(projectRoot, targetPath) {
|
|
|
99
99
|
return false;
|
|
100
100
|
return posix.startsWith("xbrief/proposed/") || posix.startsWith("vbrief/proposed/");
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Canonical + gitignored assist scratch roots for low-ceremony disposable notes (#1802).
|
|
104
|
+
* Path fence only — not free-text "for Obsidian" NLP. Tracked trees (src/, packages/, …)
|
|
105
|
+
* are never listed here.
|
|
106
|
+
*/
|
|
107
|
+
export const ASSIST_SCRATCH_ROOT_PREFIXES = [".deft-scratch/", "temp/"];
|
|
108
|
+
/**
|
|
109
|
+
* True when the write target is under an allowlisted disposable scratch root (#1802).
|
|
110
|
+
* Fail closed on null/empty/unparseable targets and on path escape (`..`).
|
|
111
|
+
* Does not authorize tracked product paths even under assist posture.
|
|
112
|
+
*/
|
|
113
|
+
export function isAllowlistedAssistScratchPath(projectRoot, targetPath) {
|
|
114
|
+
if (targetPath === null || targetPath.trim().length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
const posix = toProjectRelativePosix(projectRoot, targetPath);
|
|
117
|
+
// resolve()+relative() collapses mid-path `..`; only outside-root `..` remains.
|
|
118
|
+
if (posix === ".." || posix.startsWith("../") || isAbsolute(posix))
|
|
119
|
+
return false;
|
|
120
|
+
if (isLexicalOutsideProjectRoot(posix))
|
|
121
|
+
return false;
|
|
122
|
+
for (const prefix of ASSIST_SCRATCH_ROOT_PREFIXES) {
|
|
123
|
+
if (posix === prefix.slice(0, -1) || posix.startsWith(prefix))
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Assist/ephemeral classification for scratch-write carve-out (#1802).
|
|
130
|
+
* Requires allowlisted path AND (assist posture markers OR ephemeral spawn markers).
|
|
131
|
+
* Path alone without posture markers fails closed to the mutation gate stack.
|
|
132
|
+
*/
|
|
133
|
+
export function isAssistScratchWrite(projectRoot, targetPath, payload, environ = process.env) {
|
|
134
|
+
if (!isAllowlistedAssistScratchPath(projectRoot, targetPath))
|
|
135
|
+
return false;
|
|
136
|
+
// Structural classification only — compose with #3080 ephemeral markers.
|
|
137
|
+
if (isAssistPosture(payload, environ))
|
|
138
|
+
return true;
|
|
139
|
+
if (isEphemeralSpawn(payload))
|
|
140
|
+
return true;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
102
143
|
function isWindowsDriveOnlyRoot(value) {
|
|
103
144
|
return /^[A-Za-z]:[/\\]?$/.test(value.trim());
|
|
104
145
|
}
|
|
@@ -448,6 +489,39 @@ function decideShellOrMcpRuntimeAuthority(input, toolName, seams) {
|
|
|
448
489
|
}
|
|
449
490
|
function inspectMutationGates(input, toolName, seams, options) {
|
|
450
491
|
const projectRoot = resolve(input.projectRoot);
|
|
492
|
+
const environ = input.environ ?? process.env;
|
|
493
|
+
// Assist/scratch low-ceremony writes (#1802): allowlisted gitignored roots under
|
|
494
|
+
// assist/ephemeral classification skip ritual + active-scope (no fake scope:activate).
|
|
495
|
+
// Tracked product paths never match the path fence. Read-only still denied upstream.
|
|
496
|
+
if (!isSpawnTool(toolName)) {
|
|
497
|
+
const scratchTarget = hookWriteTargetPath(input.payload);
|
|
498
|
+
if (isAssistScratchWrite(projectRoot, scratchTarget, input.payload, environ)) {
|
|
499
|
+
const relPath = scratchTarget !== null ? toProjectRelativePosix(projectRoot, scratchTarget) : null;
|
|
500
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
501
|
+
isDirectWrite: true,
|
|
502
|
+
relPath,
|
|
503
|
+
scopePath: null,
|
|
504
|
+
});
|
|
505
|
+
if (authzDeny !== null)
|
|
506
|
+
return authzDeny;
|
|
507
|
+
const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, null);
|
|
508
|
+
if (runtimeDeny !== null)
|
|
509
|
+
return runtimeDeny;
|
|
510
|
+
return {
|
|
511
|
+
verdict: "allow",
|
|
512
|
+
code: "write-assist-scratch-ready",
|
|
513
|
+
event: input.event,
|
|
514
|
+
host: input.host,
|
|
515
|
+
toolName,
|
|
516
|
+
projectRoot,
|
|
517
|
+
message: `Directive write gate allowed ${toolName} under allowlisted assist scratch ` +
|
|
518
|
+
"root (disposable notes; active scope and story-start not required). " +
|
|
519
|
+
"Tracked product paths still require mutation gates — do not smuggle source " +
|
|
520
|
+
"under .deft-scratch/ or temp/.",
|
|
521
|
+
scopePath: null,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
451
525
|
let ritual;
|
|
452
526
|
try {
|
|
453
527
|
ritual = (seams.inspectRitual ??
|
|
@@ -520,16 +594,38 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
520
594
|
// Lexical ../ + realpath re-entry guard (not bare startsWith(".."); not symlink aliases).
|
|
521
595
|
const outsideRoot = writeTarget !== null && isOutsideProjectRootWrite(projectRoot, writeTarget);
|
|
522
596
|
if (!outsideRoot || isSpawnTool(toolName)) {
|
|
523
|
-
|
|
597
|
+
let proposedPathHint;
|
|
598
|
+
if (isSpawnTool(toolName)) {
|
|
599
|
+
// Multi-path recovery for implement-class spawns (#3080 AC4).
|
|
600
|
+
proposedPathHint =
|
|
601
|
+
" Recovery: (1) Product implementation — run `deft scope:activate -- <path>` " +
|
|
602
|
+
"for the approved xBRIEF, then re-run the pre-start_agent gate stack. " +
|
|
603
|
+
"(2) Read-only research — spawn with `subagent_type`/`worker_role` explore. " +
|
|
604
|
+
"(3) Ephemeral docs/analysis — spawn with `worker_role: ephemeral` " +
|
|
605
|
+
"(aliases: docs, assist; see commands.md), or continue in the parent without " +
|
|
606
|
+
"a lifecycle story. Do not invent a fake scope only to satisfy this gate.";
|
|
607
|
+
}
|
|
608
|
+
else if (options.proposedLifecycleExempt &&
|
|
524
609
|
relTarget !== null &&
|
|
525
|
-
(relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
|
|
526
|
-
|
|
527
|
-
"
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
610
|
+
(relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))) {
|
|
611
|
+
proposedPathHint =
|
|
612
|
+
" For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
|
|
613
|
+
"filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
|
|
614
|
+
"planning writes (#2625).";
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
proposedPathHint =
|
|
618
|
+
" Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
|
|
619
|
+
(options.proposedLifecycleExempt
|
|
620
|
+
? "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption), " +
|
|
621
|
+
"or for disposable research notes write under `.deft-scratch/` (or `temp/`) " +
|
|
622
|
+
"with assist/ephemeral posture (`DEFT_SESSION_POSTURE=assist` or " +
|
|
623
|
+
"`worker_role: assist` / ephemeral — see commands.md #1802 / #3080). " +
|
|
624
|
+
"Do not invent a fake scope only to capture Obsidian/scratch notes."
|
|
625
|
+
: "then re-run the pre-start_agent gate stack. " +
|
|
626
|
+
"For disposable research notes only: write under `.deft-scratch/` with " +
|
|
627
|
+
"assist posture (commands.md #1802) — do not fake `scope:activate` for notes.");
|
|
628
|
+
}
|
|
533
629
|
const denyCode = isSpawnTool(toolName) ? "spawn-not-ready" : "scope-not-ready";
|
|
534
630
|
return deny(input, denyCode, toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
|
|
535
631
|
}
|
|
@@ -736,6 +832,21 @@ export function decideHook(input, seams = {}) {
|
|
|
736
832
|
scopePath: null,
|
|
737
833
|
};
|
|
738
834
|
}
|
|
835
|
+
// Ephemeral/docs/assist: write-capable non-lifecycle spawn; no active xBRIEF (#3080).
|
|
836
|
+
// Does not authorize push/merge/deploy — those remain on shell/MCP matchers.
|
|
837
|
+
if (isEphemeralSpawn(input.payload)) {
|
|
838
|
+
return {
|
|
839
|
+
verdict: "allow",
|
|
840
|
+
code: "spawn-ephemeral-ready",
|
|
841
|
+
event: input.event,
|
|
842
|
+
host: input.host,
|
|
843
|
+
toolName,
|
|
844
|
+
projectRoot,
|
|
845
|
+
message: `Directive allowed ephemeral ${toolName} spawn without active-xBRIEF ` +
|
|
846
|
+
"implementation gates (non-lifecycle assist/docs posture).",
|
|
847
|
+
scopePath: null,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
739
850
|
return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: false });
|
|
740
851
|
}
|
|
741
852
|
// Shell/Bash and classifiable MCP: enforce scopes.push / scopes.merge (#2711).
|