@deftai/directive-core 0.85.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/constants.d.ts +2 -2
- package/dist/doctor/constants.js +2 -2
- package/dist/doctor/main.d.ts +8 -6
- package/dist/doctor/main.js +102 -36
- package/dist/doctor/taskfile.d.ts +8 -0
- package/dist/doctor/taskfile.js +19 -0
- package/dist/doctor/types.d.ts +3 -0
- package/dist/hooks/dispatcher.d.ts +21 -1
- package/dist/hooks/dispatcher.js +85 -15
- package/dist/init-deposit/agent-hooks.js +14 -6
- package/dist/init-deposit/gitignore.js +5 -0
- 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/policy/org-force-on-migration.js +2 -0
- package/dist/render/framework-commands.d.ts +1 -1
- package/dist/render/framework-commands.js +6 -6
- package/dist/render/roadmap-render.d.ts +5 -1
- package/dist/render/roadmap-render.js +20 -2
- package/dist/render/rule-map.js +5 -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-env/agent-hooks-live-probe.d.ts +32 -0
- package/dist/verify-env/agent-hooks-live-probe.js +216 -0
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- 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 +35 -22
- package/package.json +3 -3
package/dist/check/gate-lists.js
CHANGED
|
@@ -14,9 +14,9 @@ export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
|
14
14
|
export declare const NPM_REGISTRY_MIRROR_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md#corporate-or-mirrored-npm-registry";
|
|
15
15
|
export declare const NETWORK_DISCLOSURE_LINE: string;
|
|
16
16
|
export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
|
|
17
|
-
export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "
|
|
17
|
+
export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "xbrief"];
|
|
18
18
|
/** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
|
|
19
|
-
export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "
|
|
19
|
+
export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "xbrief"];
|
|
20
20
|
export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/agents-entry.md", "content/skills/deft-directive-build/SKILL.md"];
|
|
21
21
|
export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
|
|
22
22
|
/** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
|
package/dist/doctor/constants.js
CHANGED
|
@@ -46,9 +46,9 @@ export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (def
|
|
|
46
46
|
"(discloses tool + registry before contacting either).";
|
|
47
47
|
// Engine / lifecycle dirs that stay at the framework root (NOT relocated by
|
|
48
48
|
// #1875). Shippable-content dirs moved under content/ -- see EXPECTED_CONTENT_DIRS.
|
|
49
|
-
export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "
|
|
49
|
+
export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "xbrief"];
|
|
50
50
|
/** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
|
|
51
|
-
export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "
|
|
51
|
+
export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "xbrief"];
|
|
52
52
|
// Post-#1875 content/ move: these framework-internal markers now live under
|
|
53
53
|
// content/ in the SOURCE repo. They identify a deft source checkout (a consumer
|
|
54
54
|
// would never reproduce them); the C1 flatten means a consumer deposit has no
|
package/dist/doctor/main.d.ts
CHANGED
|
@@ -2,14 +2,16 @@ import { type ResolutionFacts } from "../resolution/index.js";
|
|
|
2
2
|
import { createPlainSink } from "./output.js";
|
|
3
3
|
import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
|
|
4
4
|
export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
|
|
5
|
-
export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams):
|
|
5
|
+
export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
|
|
6
|
+
export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
|
|
6
7
|
/**
|
|
7
8
|
* Never emit a bare `task ...` remediation in a project without Taskfile wiring
|
|
8
|
-
* (#2267). The `directive` surface always works; `task deft:X` is
|
|
9
|
-
* only
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
13
15
|
*/
|
|
14
16
|
/**
|
|
15
17
|
* Detect Taskfile wiring through the injected seam (#2267). Mirrors
|
package/dist/doctor/main.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadProjectDefinition } from "../policy/resolve.js";
|
|
|
7
7
|
import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
|
|
8
8
|
import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
|
|
9
9
|
import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
|
|
10
|
+
import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
|
|
10
11
|
import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
|
|
11
12
|
import { runChecks } from "./checks.js";
|
|
12
13
|
import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
|
|
@@ -16,10 +17,10 @@ import { pythonJsonDump } from "./json.js";
|
|
|
16
17
|
import { parseInstallRootFromAgentsMd } from "./manifest.js";
|
|
17
18
|
import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
|
|
18
19
|
import { createPlainSink } from "./output.js";
|
|
19
|
-
import { readTextSafe,
|
|
20
|
+
import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
|
|
20
21
|
import { runPayloadStalenessCheck } from "./payload-staleness.js";
|
|
21
22
|
import { runLocalSignpostChecks } from "./signpost-checks.js";
|
|
22
|
-
import { classifyTaskfileInclude, formatMissingIncludeSnippet, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
|
|
23
|
+
import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
|
|
23
24
|
import { defaultWhich } from "./which.js";
|
|
24
25
|
const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
|
|
25
26
|
/**
|
|
@@ -57,7 +58,7 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
57
58
|
const quietMode = flags.quiet;
|
|
58
59
|
const fullMode = flags.full;
|
|
59
60
|
const projectRoot = resolvePath(flags.projectRoot ?? process.cwd());
|
|
60
|
-
const frameworkRoot = seams.frameworkRoot ??
|
|
61
|
+
const frameworkRoot = seams.frameworkRoot ?? resolveFrameworkRootForProject(projectRoot);
|
|
61
62
|
const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
|
|
62
63
|
const whichFn = seams.whichFn ?? defaultWhich;
|
|
63
64
|
const nowFn = seams.now ?? (() => new Date());
|
|
@@ -165,7 +166,17 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
165
166
|
sink.blank();
|
|
166
167
|
}
|
|
167
168
|
sink.info("Checking agent-host hook registration...");
|
|
168
|
-
|
|
169
|
+
const findingCountBeforeHooks = findings.length;
|
|
170
|
+
const hooksHealthy = runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
|
|
171
|
+
if (fullMode && hooksHealthy) {
|
|
172
|
+
const lastFinding = findings[findings.length - 1];
|
|
173
|
+
if (findings.length > findingCountBeforeHooks &&
|
|
174
|
+
lastFinding?.check === "agent-hooks-registration" &&
|
|
175
|
+
lastFinding?.status === "registered") {
|
|
176
|
+
findings.pop();
|
|
177
|
+
}
|
|
178
|
+
runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams);
|
|
179
|
+
}
|
|
169
180
|
if (consumerContext) {
|
|
170
181
|
if (!jsonMode) {
|
|
171
182
|
sink.blank();
|
|
@@ -280,7 +291,7 @@ export function cmdDoctor(args, seams = {}) {
|
|
|
280
291
|
if (!jsonMode) {
|
|
281
292
|
sink.blank();
|
|
282
293
|
}
|
|
283
|
-
sink.info("Checking
|
|
294
|
+
sink.info("Checking gates-surface readiness (Taskfile include for deep-think agent gates)...");
|
|
284
295
|
runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams);
|
|
285
296
|
let resolution = null;
|
|
286
297
|
if (!runningInsideDeftRepo(projectRoot, seams)) {
|
|
@@ -353,40 +364,81 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
|
|
|
353
364
|
const reason = "maintainer source checkout; project hook deposit is consumer-only";
|
|
354
365
|
sink.info(`${checkName}: skip -- ${reason}`);
|
|
355
366
|
addFinding({ severity: "skip", message: reason, check: checkName, status: "skip" });
|
|
356
|
-
return;
|
|
367
|
+
return false;
|
|
357
368
|
}
|
|
358
369
|
try {
|
|
359
370
|
const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
|
|
360
|
-
if (result.code
|
|
361
|
-
const message = `${checkName}:
|
|
362
|
-
|
|
363
|
-
sink.success(message);
|
|
371
|
+
if (result.code !== 0) {
|
|
372
|
+
const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
|
|
373
|
+
sink.warn(message);
|
|
364
374
|
addFinding({
|
|
365
|
-
severity: "
|
|
375
|
+
severity: "warning",
|
|
366
376
|
message,
|
|
367
377
|
check: checkName,
|
|
368
|
-
status: "
|
|
378
|
+
status: result.code === 2 ? "unavailable" : "incomplete",
|
|
369
379
|
registrations: result.registrations,
|
|
370
|
-
|
|
371
|
-
trust_review: "Open `/hooks` in Codex and review the project hook commands.",
|
|
380
|
+
suggestion: "deft update",
|
|
372
381
|
});
|
|
373
|
-
return;
|
|
382
|
+
return false;
|
|
374
383
|
}
|
|
375
|
-
const message = `${checkName}:
|
|
376
|
-
|
|
384
|
+
const message = `${checkName}: registered and structurally valid; ` +
|
|
385
|
+
"Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
|
|
386
|
+
sink.success(message);
|
|
377
387
|
addFinding({
|
|
378
|
-
severity: "
|
|
388
|
+
severity: "skip",
|
|
379
389
|
message,
|
|
380
390
|
check: checkName,
|
|
381
|
-
status:
|
|
391
|
+
status: "registered",
|
|
382
392
|
registrations: result.registrations,
|
|
383
|
-
|
|
393
|
+
trust_status: "not-verifiable",
|
|
394
|
+
trust_review: "Open `/hooks` in Codex and review the project hook commands.",
|
|
384
395
|
});
|
|
396
|
+
return true;
|
|
385
397
|
}
|
|
386
398
|
catch (cause) {
|
|
387
399
|
const message = `${checkName}: probe failed -- ${String(cause)}`;
|
|
388
400
|
sink.warn(message);
|
|
389
401
|
addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams) {
|
|
406
|
+
const checkName = "agent-hooks-registration";
|
|
407
|
+
const liveCheckName = "agent-hooks-live-probe";
|
|
408
|
+
try {
|
|
409
|
+
const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
|
|
410
|
+
const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot);
|
|
411
|
+
if (liveResult.code !== 0) {
|
|
412
|
+
const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
|
|
413
|
+
sink.warn(message);
|
|
414
|
+
addFinding({
|
|
415
|
+
severity: "warning",
|
|
416
|
+
message,
|
|
417
|
+
check: liveCheckName,
|
|
418
|
+
status: liveResult.code === 2 ? "unavailable" : "non-functional",
|
|
419
|
+
cases: liveResult.cases,
|
|
420
|
+
suggestion: "npm i -g @deftai/directive@latest && deft update",
|
|
421
|
+
});
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const message = `${checkName}: registered, structurally valid, and live probe passed; ` +
|
|
425
|
+
"Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
|
|
426
|
+
sink.success(message);
|
|
427
|
+
addFinding({
|
|
428
|
+
severity: "skip",
|
|
429
|
+
message,
|
|
430
|
+
check: liveCheckName,
|
|
431
|
+
status: "registered-and-functional",
|
|
432
|
+
registrations: result.registrations,
|
|
433
|
+
trust_status: "not-verifiable",
|
|
434
|
+
trust_review: "Open `/hooks` in Codex and review the project hook commands.",
|
|
435
|
+
live_probe: "passed",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch (cause) {
|
|
439
|
+
const message = `${liveCheckName}: probe failed -- ${String(cause)}`;
|
|
440
|
+
sink.warn(message);
|
|
441
|
+
addFinding({ severity: "warning", message, check: liveCheckName, suggestion: "deft update" });
|
|
390
442
|
}
|
|
391
443
|
}
|
|
392
444
|
function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
|
|
@@ -588,15 +640,21 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
588
640
|
}
|
|
589
641
|
const includeStatus = classifyTaskfileInclude(projectRoot);
|
|
590
642
|
if (includeStatus === "ok") {
|
|
591
|
-
sink.success("
|
|
643
|
+
sink.success("Gates-surface ready: root Taskfile.yml includes the deft framework (`task deft:<verb>`)");
|
|
592
644
|
return;
|
|
593
645
|
}
|
|
594
646
|
if (includeStatus === "missing-file") {
|
|
595
647
|
let includeMissing = true;
|
|
596
648
|
const target = join(projectRoot, "Taskfile.yml");
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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);
|
|
600
658
|
if (!jsonMode) {
|
|
601
659
|
sink.blank();
|
|
602
660
|
sink.raw(TASKFILE_INCLUDE_SNIPPET);
|
|
@@ -616,22 +674,27 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
616
674
|
}
|
|
617
675
|
}
|
|
618
676
|
else {
|
|
619
|
-
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.");
|
|
620
678
|
}
|
|
621
679
|
}
|
|
622
680
|
if (includeMissing) {
|
|
623
681
|
addFinding({
|
|
624
682
|
severity: "warning",
|
|
625
|
-
message: "
|
|
683
|
+
message: "Gates-surface: root Taskfile.yml missing — deep-think gates need `deft` CLI or `task deft:` include",
|
|
626
684
|
check: "taskfile-include",
|
|
627
685
|
file: target,
|
|
628
|
-
suggestion:
|
|
686
|
+
suggestion: formatGatesSurfaceDualRemediation("missing-file"),
|
|
629
687
|
});
|
|
630
688
|
}
|
|
631
689
|
return;
|
|
632
690
|
}
|
|
633
691
|
if (includeStatus === "missing-include") {
|
|
634
|
-
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:`):";
|
|
635
698
|
sink.warn(message);
|
|
636
699
|
if (!jsonMode) {
|
|
637
700
|
sink.blank();
|
|
@@ -640,21 +703,23 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
|
|
|
640
703
|
const tf = resolveConsumerTaskfile(projectRoot);
|
|
641
704
|
addFinding({
|
|
642
705
|
severity: "warning",
|
|
643
|
-
message: "
|
|
706
|
+
message: "Gates-surface: root Taskfile.yml does not include the deft framework — deep-think gates need `deft` CLI or `task deft:` include",
|
|
644
707
|
check: "taskfile-include",
|
|
645
708
|
file: tf,
|
|
646
|
-
suggestion:
|
|
709
|
+
suggestion: formatGatesSurfaceDualRemediation("missing-include"),
|
|
647
710
|
});
|
|
648
711
|
return;
|
|
649
712
|
}
|
|
650
713
|
const taskfilePath = resolveConsumerTaskfile(projectRoot) ?? join(projectRoot, "Taskfile.yml");
|
|
651
|
-
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.";
|
|
652
716
|
sink.warn(message);
|
|
653
717
|
addFinding({
|
|
654
718
|
severity: "warning",
|
|
655
719
|
message,
|
|
656
720
|
check: "taskfile-include",
|
|
657
721
|
file: taskfilePath,
|
|
722
|
+
suggestion: GATES_SURFACE_DEFT_REMEDIATION,
|
|
658
723
|
});
|
|
659
724
|
}
|
|
660
725
|
/**
|
|
@@ -734,11 +799,12 @@ function runPlanExtensionShadowCheck(projectRoot, sink, addFinding, seams) {
|
|
|
734
799
|
}
|
|
735
800
|
/**
|
|
736
801
|
* Never emit a bare `task ...` remediation in a project without Taskfile wiring
|
|
737
|
-
* (#2267). The `directive` surface always works; `task deft:X` is
|
|
738
|
-
* only
|
|
739
|
-
* `
|
|
740
|
-
*
|
|
741
|
-
*
|
|
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.
|
|
742
808
|
*/
|
|
743
809
|
/**
|
|
744
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
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { EngineProbeResult } from "../resolution/classify.js";
|
|
|
4
4
|
import type { ResolutionMode } from "../resolution/index.js";
|
|
5
5
|
import type { ResolveUserMdResult } from "../user-config/resolve-user-md.js";
|
|
6
6
|
import type { AgentHookHealthResult } from "../verify-env/agent-hooks.js";
|
|
7
|
+
import type { AgentHookLiveProbeResult } from "../verify-env/agent-hooks-live-probe.js";
|
|
7
8
|
export declare const EXIT_CLEAN = 0;
|
|
8
9
|
export declare const EXIT_DRIFT = 1;
|
|
9
10
|
export declare const EXIT_CONFIG_ERROR = 2;
|
|
@@ -147,5 +148,7 @@ export interface DoctorSeams {
|
|
|
147
148
|
readonly detectPlanExtensionShadows?: (projectRoot: string) => readonly ShadowedPlanExtension[];
|
|
148
149
|
/** Read-only agent-host hook registration probe (#2438). */
|
|
149
150
|
readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
|
|
151
|
+
/** Live hook spawn probe for doctor --full (#2852). */
|
|
152
|
+
readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
|
|
150
153
|
}
|
|
151
154
|
//# sourceMappingURL=types.d.ts.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
|
}
|
|
@@ -238,14 +238,22 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
|
|
|
238
238
|
changedPaths.push(item.path);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
const legacyAdapterPaths = [
|
|
242
|
+
".cursor/hooks/deft-cursor-hook-adapter.mjs",
|
|
243
|
+
".cursor/hooks/deft-cursor-hook-adapter.test.mjs",
|
|
244
|
+
];
|
|
245
|
+
let removedLegacyAdapter = false;
|
|
246
|
+
for (const relative of legacyAdapterPaths) {
|
|
247
|
+
const absolute = join(projectRoot, relative);
|
|
248
|
+
assertDepositContained(projectRoot, absolute);
|
|
249
|
+
if (existsSync(absolute)) {
|
|
250
|
+
rmSync(absolute, { force: true });
|
|
251
|
+
removedLegacyAdapter = true;
|
|
247
252
|
}
|
|
248
253
|
}
|
|
254
|
+
if (removedLegacyAdapter && !changedPaths.includes(AGENT_HOOK_PATHS[2])) {
|
|
255
|
+
changedPaths.push(AGENT_HOOK_PATHS[2]);
|
|
256
|
+
}
|
|
249
257
|
if (changedPaths.length > 0) {
|
|
250
258
|
io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
|
|
251
259
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { execFileSync } from "node:child_process";
|
|
12
12
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
+
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
14
15
|
import { FORBIDDEN_BLANKET_EVAL_LINES, stripGitignoreInlineComment, } from "../triage/bootstrap/gitignore.js";
|
|
15
16
|
/** Directory ignore entry for the hybrid deposit (greenfield only). */
|
|
16
17
|
export const GITIGNORE_DEFT_CORE_LINE = ".deft/core/";
|
|
@@ -195,9 +196,13 @@ function reconcileGitignoreFile(projectDir, targetLines, includeDeftCoreRational
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
try {
|
|
199
|
+
assertWriteTargetSafe(projectDir, path);
|
|
198
200
|
writeFileSync(path, body, { encoding: "utf8", mode: 0o644 });
|
|
199
201
|
}
|
|
200
202
|
catch (cause) {
|
|
203
|
+
if (cause instanceof ProjectionContainmentError) {
|
|
204
|
+
throw cause;
|
|
205
|
+
}
|
|
201
206
|
throw new Error(`could not write .gitignore: ${String(cause)}`);
|
|
202
207
|
}
|
|
203
208
|
const finalPresent = collectPresentGitignoreLines(body);
|