@deftai/directive-core 0.78.0 → 0.79.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/empty-populate.d.ts +44 -0
- package/dist/cache/empty-populate.js +117 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/check-updates/index.d.ts +10 -0
- package/dist/check-updates/index.js +81 -1
- package/dist/doctor/main.js +13 -2
- package/dist/doctor/payload-staleness.js +7 -1
- package/dist/hooks/dispatcher.d.ts +14 -2
- package/dist/hooks/dispatcher.js +77 -20
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +10 -0
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/init-deposit/init-deposit.js +2 -0
- package/dist/init-deposit/prettierignore.d.ts +21 -0
- package/dist/init-deposit/prettierignore.js +83 -0
- package/dist/init-deposit/refresh.d.ts +3 -1
- package/dist/init-deposit/refresh.js +29 -40
- package/dist/init-deposit/scaffold.js +6 -12
- package/dist/init-deposit/xbrief-projections.d.ts +18 -0
- package/dist/init-deposit/xbrief-projections.js +117 -0
- package/dist/intake/reconcile-issues.d.ts +10 -0
- package/dist/intake/reconcile-issues.js +121 -3
- package/dist/issue-sync/index.d.ts +2 -0
- package/dist/issue-sync/index.js +2 -0
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
- package/dist/issue-sync/sync-from-xbrief.js +208 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +5 -0
- package/dist/platform/platform-capabilities.js +9 -0
- package/dist/platform/shell-context.d.ts +30 -0
- package/dist/platform/shell-context.js +111 -0
- package/dist/pr-monitor/cadence.d.ts +6 -0
- package/dist/pr-monitor/cadence.js +13 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +7 -0
- package/dist/pr-monitor/monitor.js +59 -13
- package/dist/pr-wait-mergeable/wrappers.d.ts +9 -0
- package/dist/pr-wait-mergeable/wrappers.js +31 -2
- package/dist/preflight-cache/evaluate.d.ts +3 -8
- package/dist/preflight-cache/evaluate.js +19 -19
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +10 -1
- package/dist/release/flags.js +26 -0
- package/dist/release/issue-state-fetch.d.ts +27 -0
- package/dist/release/issue-state-fetch.js +146 -0
- package/dist/release/main.js +1 -0
- package/dist/release/native-steps.js +6 -4
- package/dist/release/pipeline.js +15 -5
- package/dist/release/preflight.d.ts +6 -2
- package/dist/release/preflight.js +19 -5
- package/dist/release/types.d.ts +3 -1
- package/dist/release-e2e/constants.js +6 -6
- package/dist/release-e2e/flags.d.ts +2 -0
- package/dist/release-e2e/flags.js +8 -4
- package/dist/release-e2e/main.js +15 -9
- package/dist/release-e2e/types.d.ts +4 -2
- package/dist/resolution/classify.d.ts +1 -1
- package/dist/resolution/classify.js +36 -11
- package/dist/scope/project-definition-sync.js +2 -150
- package/dist/scope/registry-artifact-sync.d.ts +3 -0
- package/dist/scope/registry-artifact-sync.js +159 -0
- package/dist/scope/specification-sync.d.ts +3 -0
- package/dist/scope/specification-sync.js +16 -0
- package/dist/scope/transition.js +40 -12
- package/dist/session/cache-recovery.d.ts +19 -0
- package/dist/session/cache-recovery.js +26 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +14 -3
- package/dist/session/verify-session-ritual.d.ts +1 -0
- package/dist/session/verify-session-ritual.js +1 -0
- package/dist/triage/welcome/default-mode.js +2 -0
- package/dist/triage/welcome/onboard.js +2 -0
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +9 -0
- package/dist/vbrief-reconcile/labels.d.ts +3 -0
- package/dist/vbrief-reconcile/labels.js +14 -0
- package/dist/vbrief-reconcile/main.d.ts +1 -0
- package/dist/vbrief-reconcile/main.js +13 -3
- package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
- package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
- package/dist/vbrief-reconcile/repo-guard.js +33 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
- package/dist/vbrief-reconcile/umbrellas.js +17 -1
- package/dist/verify-env/agent-hooks.js +4 -3
- package/dist/verify-env/verify-hooks-installed.js +5 -5
- package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
- package/dist/vitest-runner/coverage-debt.d.ts +50 -0
- package/dist/vitest-runner/coverage-debt.js +155 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
- package/dist/xbrief-migrate/detect.js +46 -1
- package/dist/xbrief-migrate/migrate-project.js +19 -4
- package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
- package/dist/xbrief-migrate/migration-containment.js +37 -0
- package/package.json +7 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* empty-populate.ts -- auto-fetch GitHub when the triage cache is empty (#2575).
|
|
3
|
+
*
|
|
4
|
+
* Empty/missing cache is the one case where a live list beats a false-empty
|
|
5
|
+
* "what's next?" answer. Non-empty caches keep the 24h freshness gate unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import type { FetchAllReportImpl } from "./fetch.js";
|
|
8
|
+
export interface EmptyPopulateResult {
|
|
9
|
+
readonly skipped: boolean;
|
|
10
|
+
readonly skipReason: string | null;
|
|
11
|
+
readonly repo: string | null;
|
|
12
|
+
readonly populated: boolean;
|
|
13
|
+
readonly succeeded: number | null;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface EmptyPopulateOptions {
|
|
17
|
+
readonly repo?: string | null;
|
|
18
|
+
readonly cacheRoot?: string;
|
|
19
|
+
readonly source?: string;
|
|
20
|
+
readonly inferRepoFn?: (cwd: string) => string | null;
|
|
21
|
+
readonly fetchFn?: (options: {
|
|
22
|
+
source: string;
|
|
23
|
+
repo: string;
|
|
24
|
+
cacheRoot: string;
|
|
25
|
+
force: boolean;
|
|
26
|
+
}) => FetchAllReportImpl;
|
|
27
|
+
readonly seedFn?: (projectRoot: string) => {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
};
|
|
30
|
+
readonly backfillFn?: (projectRoot: string, repo: string | null) => {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** True when `.deft-cache/github-issue/` has zero cached issue entries. */
|
|
35
|
+
export declare function isTriageCacheEmpty(projectRoot: string, cacheRoot?: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* When the triage cache is empty, mirror `triage:bootstrap` populate steps:
|
|
38
|
+
* fetch-all from GitHub, seed the candidates log, backfill lifecycle accepts.
|
|
39
|
+
* Never auto-accepts new issues beyond existing xBRIEF backfill.
|
|
40
|
+
*/
|
|
41
|
+
export declare function maybeAutoPopulateEmptyCache(projectRoot: string, options?: EmptyPopulateOptions): EmptyPopulateResult;
|
|
42
|
+
/** Idempotent hydrate hook for triage read paths (#2575). */
|
|
43
|
+
export declare function ensureTriageCacheHydrated(projectRoot: string, options?: EmptyPopulateOptions): EmptyPopulateResult;
|
|
44
|
+
//# sourceMappingURL=empty-populate.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* empty-populate.ts -- auto-fetch GitHub when the triage cache is empty (#2575).
|
|
3
|
+
*
|
|
4
|
+
* Empty/missing cache is the one case where a live list beats a false-empty
|
|
5
|
+
* "what's next?" answer. Non-empty caches keep the 24h freshness gate unchanged.
|
|
6
|
+
*/
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import { stepSeedCandidatesLog } from "../triage/bootstrap/gitignore.js";
|
|
9
|
+
import { inferRepoFromGit, stepBackfillAuditLog } from "../triage/bootstrap/index.js";
|
|
10
|
+
import { ENV_TRIAGE_REPO } from "../triage/queue/constants.js";
|
|
11
|
+
import { CACHE_DIR_NAME, CACHE_SOURCE, iterCachedIssues } from "../triage/summary/index.js";
|
|
12
|
+
import { cacheFetchAll } from "./fetch.js";
|
|
13
|
+
/** True when `.deft-cache/github-issue/` has zero cached issue entries. */
|
|
14
|
+
export function isTriageCacheEmpty(projectRoot, cacheRoot) {
|
|
15
|
+
const root = resolve(projectRoot);
|
|
16
|
+
const resolvedCacheRoot = cacheRoot ?? join(root, CACHE_DIR_NAME);
|
|
17
|
+
return iterCachedIssues(resolvedCacheRoot).length === 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* When the triage cache is empty, mirror `triage:bootstrap` populate steps:
|
|
21
|
+
* fetch-all from GitHub, seed the candidates log, backfill lifecycle accepts.
|
|
22
|
+
* Never auto-accepts new issues beyond existing xBRIEF backfill.
|
|
23
|
+
*/
|
|
24
|
+
export function maybeAutoPopulateEmptyCache(projectRoot, options = {}) {
|
|
25
|
+
const root = resolve(projectRoot);
|
|
26
|
+
const source = options.source ?? CACHE_SOURCE;
|
|
27
|
+
const cacheRoot = options.cacheRoot ?? join(root, CACHE_DIR_NAME);
|
|
28
|
+
if (!isTriageCacheEmpty(root, cacheRoot)) {
|
|
29
|
+
return {
|
|
30
|
+
skipped: true,
|
|
31
|
+
skipReason: "cache-non-empty",
|
|
32
|
+
repo: null,
|
|
33
|
+
populated: false,
|
|
34
|
+
succeeded: null,
|
|
35
|
+
message: "cache already populated",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const infer = options.inferRepoFn ?? inferRepoFromGit;
|
|
39
|
+
const envRepo = (process.env[ENV_TRIAGE_REPO] ?? "").trim();
|
|
40
|
+
const repo = options.repo ?? (envRepo.length > 0 ? envRepo : null) ?? infer(root);
|
|
41
|
+
if (repo === null) {
|
|
42
|
+
return {
|
|
43
|
+
skipped: true,
|
|
44
|
+
skipReason: "repo-not-resolved",
|
|
45
|
+
repo: null,
|
|
46
|
+
populated: false,
|
|
47
|
+
succeeded: null,
|
|
48
|
+
message: "could not infer repo for empty-cache auto-populate",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const fetch = options.fetchFn ?? cacheFetchAll;
|
|
52
|
+
let report;
|
|
53
|
+
try {
|
|
54
|
+
report = fetch({ source, repo, cacheRoot, force: true });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return {
|
|
59
|
+
skipped: true,
|
|
60
|
+
skipReason: "fetch-failed",
|
|
61
|
+
repo,
|
|
62
|
+
populated: false,
|
|
63
|
+
succeeded: null,
|
|
64
|
+
message,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const seed = options.seedFn ?? stepSeedCandidatesLog;
|
|
68
|
+
const seedOutcome = seed(root);
|
|
69
|
+
if (!seedOutcome.ok) {
|
|
70
|
+
return {
|
|
71
|
+
skipped: true,
|
|
72
|
+
skipReason: "seed-failed",
|
|
73
|
+
repo,
|
|
74
|
+
populated: false,
|
|
75
|
+
succeeded: report.issuesWritten,
|
|
76
|
+
message: "candidates log seed failed after fetch-all",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const backfill = options.backfillFn ?? stepBackfillAuditLog;
|
|
80
|
+
try {
|
|
81
|
+
const backfillOutcome = backfill(root, repo);
|
|
82
|
+
if (!backfillOutcome.ok) {
|
|
83
|
+
return {
|
|
84
|
+
skipped: true,
|
|
85
|
+
skipReason: "backfill-failed",
|
|
86
|
+
repo,
|
|
87
|
+
populated: false,
|
|
88
|
+
succeeded: report.issuesWritten,
|
|
89
|
+
message: "audit log backfill failed after fetch-all",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
return {
|
|
96
|
+
skipped: true,
|
|
97
|
+
skipReason: "backfill-failed",
|
|
98
|
+
repo,
|
|
99
|
+
populated: false,
|
|
100
|
+
succeeded: report.issuesWritten,
|
|
101
|
+
message,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
skipped: false,
|
|
106
|
+
skipReason: null,
|
|
107
|
+
repo,
|
|
108
|
+
populated: true,
|
|
109
|
+
succeeded: report.issuesWritten,
|
|
110
|
+
message: `auto-populated empty triage cache from GitHub (${repo})`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Idempotent hydrate hook for triage read paths (#2575). */
|
|
114
|
+
export function ensureTriageCacheHydrated(projectRoot, options = {}) {
|
|
115
|
+
return maybeAutoPopulateEmptyCache(projectRoot, options);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=empty-populate.js.map
|
package/dist/cache/index.d.ts
CHANGED
package/dist/cache/index.js
CHANGED
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
export declare const REMOTE_PROBE_DEFAULT_TIMEOUT = 5;
|
|
3
3
|
/** Baked-in canonical upstream (#1320). Never probe consumer origin. */
|
|
4
4
|
export declare const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
|
|
5
|
+
/** SSH form of the canonical upstream — allowlisted alongside HTTPS (#2601). */
|
|
6
|
+
export declare const DEFT_UPSTREAM_SSH_URL = "git@github.com:deftai/directive.git";
|
|
5
7
|
/** Reject git ls-remote targets that could be parsed as options (#CodeQL). */
|
|
6
8
|
export declare function isSafeGitLsRemoteTarget(url: string): boolean;
|
|
7
9
|
/** Return a trimmed upstream URL safe for `git ls-remote`, or null when rejected. */
|
|
8
10
|
export declare function sanitizeGitLsRemoteTarget(url: string): string | null;
|
|
11
|
+
/** Extract hostname from an https or git@ upstream URL. */
|
|
12
|
+
export declare function extractUpstreamHostname(url: string): string | null;
|
|
13
|
+
/** Reject localhost, link-local, and RFC1918 targets for blind SSRF hardening (#2601). */
|
|
14
|
+
export declare function isPrivateOrLinkLocalUpstreamHost(hostname: string): boolean;
|
|
15
|
+
/** True when a sanitized upstream URL is on the canonical allowlist (#2601). */
|
|
16
|
+
export declare function isAllowlistedSafeUpstreamUrl(safeUrl: string): boolean;
|
|
17
|
+
/** True when a manifest-controlled upstream URL is safe to probe (#2601). */
|
|
18
|
+
export declare function isAllowlistedUpstreamUrl(url: string): boolean;
|
|
9
19
|
export type RemoteProbeStatus = "ok" | "behind" | "skipped" | "no-upstream" | "no-tags" | "error";
|
|
10
20
|
export interface RemoteProbeResult {
|
|
11
21
|
readonly status: RemoteProbeStatus;
|
|
@@ -7,6 +7,9 @@ import { resolveVersion } from "../doctor/paths.js";
|
|
|
7
7
|
export const REMOTE_PROBE_DEFAULT_TIMEOUT = 5.0;
|
|
8
8
|
/** Baked-in canonical upstream (#1320). Never probe consumer origin. */
|
|
9
9
|
export const DEFT_UPSTREAM_URL = "https://github.com/deftai/directive.git";
|
|
10
|
+
/** SSH form of the canonical upstream — allowlisted alongside HTTPS (#2601). */
|
|
11
|
+
export const DEFT_UPSTREAM_SSH_URL = "git@github.com:deftai/directive.git";
|
|
12
|
+
const ALLOWED_UPSTREAM_URLS = new Set([DEFT_UPSTREAM_URL, DEFT_UPSTREAM_SSH_URL]);
|
|
10
13
|
const MANIFEST_UPSTREAM_URL_KEYS = [
|
|
11
14
|
"source_url",
|
|
12
15
|
"url",
|
|
@@ -32,6 +35,83 @@ export function sanitizeGitLsRemoteTarget(url) {
|
|
|
32
35
|
}
|
|
33
36
|
return null;
|
|
34
37
|
}
|
|
38
|
+
/** Extract hostname from an https or git@ upstream URL. */
|
|
39
|
+
export function extractUpstreamHostname(url) {
|
|
40
|
+
const trimmed = url.trim();
|
|
41
|
+
const httpsMatch = /^https?:\/\/([^/:?#]+)/i.exec(trimmed);
|
|
42
|
+
if (httpsMatch?.[1]) {
|
|
43
|
+
return httpsMatch[1].toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
const sshMatch = /^git@([^:/]+):/i.exec(trimmed);
|
|
46
|
+
if (sshMatch?.[1]) {
|
|
47
|
+
return sshMatch[1].toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function parseIpv4Octets(hostname) {
|
|
52
|
+
const parts = hostname.split(".");
|
|
53
|
+
if (parts.length !== 4) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const octets = [];
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (!/^\d+$/.test(part)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const value = Number(part);
|
|
62
|
+
if (value > 255) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
octets.push(value);
|
|
66
|
+
}
|
|
67
|
+
return octets;
|
|
68
|
+
}
|
|
69
|
+
/** Reject localhost, link-local, and RFC1918 targets for blind SSRF hardening (#2601). */
|
|
70
|
+
export function isPrivateOrLinkLocalUpstreamHost(hostname) {
|
|
71
|
+
const host = hostname.trim().toLowerCase();
|
|
72
|
+
if (host.length === 0) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (host === "::1" || host === "[::1]") {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const octets = parseIpv4Octets(host);
|
|
82
|
+
if (octets !== null) {
|
|
83
|
+
const [a, b] = octets;
|
|
84
|
+
if (a === 127)
|
|
85
|
+
return true;
|
|
86
|
+
if (a === 10)
|
|
87
|
+
return true;
|
|
88
|
+
if (a === 172 && b !== undefined && b >= 16 && b <= 31)
|
|
89
|
+
return true;
|
|
90
|
+
if (a === 192 && b === 168)
|
|
91
|
+
return true;
|
|
92
|
+
if (a === 169 && b === 254)
|
|
93
|
+
return true;
|
|
94
|
+
if (a === 0)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
/** True when a sanitized upstream URL is on the canonical allowlist (#2601). */
|
|
100
|
+
export function isAllowlistedSafeUpstreamUrl(safeUrl) {
|
|
101
|
+
if (!ALLOWED_UPSTREAM_URLS.has(safeUrl)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const hostname = extractUpstreamHostname(safeUrl);
|
|
105
|
+
if (hostname === null || isPrivateOrLinkLocalUpstreamHost(hostname)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
/** True when a manifest-controlled upstream URL is safe to probe (#2601). */
|
|
111
|
+
export function isAllowlistedUpstreamUrl(url) {
|
|
112
|
+
const safe = sanitizeGitLsRemoteTarget(url);
|
|
113
|
+
return safe !== null && isAllowlistedSafeUpstreamUrl(safe);
|
|
114
|
+
}
|
|
35
115
|
function normalizePrereleaseForSort(pre) {
|
|
36
116
|
const dot = pre.indexOf(".");
|
|
37
117
|
if (dot <= 0) {
|
|
@@ -134,7 +214,7 @@ export function resolveUpstreamUrl(projectRoot) {
|
|
|
134
214
|
const value = manifest[key];
|
|
135
215
|
if (typeof value === "string" && value.trim()) {
|
|
136
216
|
const safe = sanitizeGitLsRemoteTarget(value);
|
|
137
|
-
if (safe) {
|
|
217
|
+
if (safe && isAllowlistedSafeUpstreamUrl(safe)) {
|
|
138
218
|
return safe;
|
|
139
219
|
}
|
|
140
220
|
}
|
package/dist/doctor/main.js
CHANGED
|
@@ -349,7 +349,18 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
|
|
|
349
349
|
try {
|
|
350
350
|
const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
|
|
351
351
|
if (result.code === 0) {
|
|
352
|
-
|
|
352
|
+
const message = `${checkName}: registered and structurally valid; ` +
|
|
353
|
+
"Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
|
|
354
|
+
sink.success(message);
|
|
355
|
+
addFinding({
|
|
356
|
+
severity: "skip",
|
|
357
|
+
message,
|
|
358
|
+
check: checkName,
|
|
359
|
+
status: "registered",
|
|
360
|
+
registrations: result.registrations,
|
|
361
|
+
trust_status: "not-verifiable",
|
|
362
|
+
trust_review: "Open `/hooks` in Codex and review the project hook commands.",
|
|
363
|
+
});
|
|
353
364
|
return;
|
|
354
365
|
}
|
|
355
366
|
const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
|
|
@@ -358,7 +369,7 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
|
|
|
358
369
|
severity: "warning",
|
|
359
370
|
message,
|
|
360
371
|
check: checkName,
|
|
361
|
-
status: result.code === 2 ? "unavailable" : "
|
|
372
|
+
status: result.code === 2 ? "unavailable" : "incomplete",
|
|
362
373
|
registrations: result.registrations,
|
|
363
374
|
suggestion: "deft update",
|
|
364
375
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/migrate.js";
|
|
4
|
+
import { isPublishable } from "../release/version.js";
|
|
4
5
|
import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
|
|
5
6
|
import { locateManifest, parseInstallManifest } from "./manifest.js";
|
|
6
7
|
import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
|
|
@@ -203,7 +204,12 @@ export function runPayloadStalenessCheck(projectRoot, sink, addFinding, seams =
|
|
|
203
204
|
});
|
|
204
205
|
return;
|
|
205
206
|
}
|
|
206
|
-
const
|
|
207
|
+
const normalizedRef = ref.trim().replace(/^refs\/tags\//, "");
|
|
208
|
+
const normalizedTag = tag.trim().replace(/^refs\/tags\//, "");
|
|
209
|
+
// Branch pins (ref not a publishable tag) must not fall through to npm via a stale tag (#2538).
|
|
210
|
+
const installedCandidate = isPublishable(normalizedRef)
|
|
211
|
+
? normalizedTag || normalizedRef
|
|
212
|
+
: normalizedRef;
|
|
207
213
|
const applicability = evaluateReleaseAvailability(installedCandidate, null);
|
|
208
214
|
if (applicability.status === "not-applicable") {
|
|
209
215
|
if (remoteSha && installedSha === remoteSha) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type VerifyResult } from "../session/verify-session-ritual.js";
|
|
2
2
|
import { type ActiveScopeInspection } from "./scope.js";
|
|
3
3
|
export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
|
|
4
|
-
export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor"];
|
|
4
|
+
export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
|
|
5
5
|
export type HookHost = (typeof HOOK_HOSTS)[number];
|
|
6
6
|
export declare const HOOK_EVENTS: readonly ["session.start", "tool.before"];
|
|
7
7
|
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
8
8
|
export type HookVerdict = "allow" | "deny";
|
|
9
|
-
export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-ready";
|
|
9
|
+
export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready";
|
|
10
10
|
export interface HookDecision {
|
|
11
11
|
readonly verdict: HookVerdict;
|
|
12
12
|
readonly code: HookDecisionCode;
|
|
@@ -33,6 +33,18 @@ export interface HookPolicySeams {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
export declare function hookToolName(payload: unknown): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
38
|
+
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
39
|
+
*/
|
|
40
|
+
export declare function hookWriteTargetPath(payload: unknown): string | null;
|
|
41
|
+
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
42
|
+
export declare function toProjectRelativePosix(projectRoot: string, targetPath: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
45
|
+
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
46
|
+
*/
|
|
47
|
+
export declare function isProposedLifecycleWrite(projectRoot: string, targetPath: string | null): boolean;
|
|
36
48
|
export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
|
|
37
49
|
export declare function isHookHost(value: string): value is HookHost;
|
|
38
50
|
export declare function isHookEvent(value: string): value is HookEvent;
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
2
3
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
3
4
|
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
4
5
|
import { inspectActiveScope } from "./scope.js";
|
|
5
6
|
import { isDirectWriteTool } from "./tools.js";
|
|
6
7
|
export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
|
|
7
|
-
export const HOOK_HOSTS = ["claude", "grok", "cursor"];
|
|
8
|
+
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
8
9
|
export const HOOK_EVENTS = ["session.start", "tool.before"];
|
|
9
10
|
function record(value) {
|
|
10
11
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -24,6 +25,39 @@ export function hookToolName(payload) {
|
|
|
24
25
|
return null;
|
|
25
26
|
return firstString(input.tool_name, input.toolName, input.tool);
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
30
|
+
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
31
|
+
*/
|
|
32
|
+
export function hookWriteTargetPath(payload) {
|
|
33
|
+
const input = record(payload);
|
|
34
|
+
if (input === null)
|
|
35
|
+
return null;
|
|
36
|
+
const toolInput = record(input.tool_input) ?? record(input.toolInput) ?? record(input.input);
|
|
37
|
+
return firstString(toolInput?.file_path, toolInput?.filePath, toolInput?.path, input.file_path, input.filePath, input.path);
|
|
38
|
+
}
|
|
39
|
+
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
40
|
+
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
41
|
+
const abs = resolve(projectRoot, targetPath);
|
|
42
|
+
const rel = relative(resolve(projectRoot), abs);
|
|
43
|
+
return rel.split(sep).join("/");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
47
|
+
* planning, not implementation dispatch — exempt from the active-scope gate (#2625).
|
|
48
|
+
*/
|
|
49
|
+
export function isProposedLifecycleWrite(projectRoot, targetPath) {
|
|
50
|
+
if (targetPath === null || targetPath.trim().length === 0)
|
|
51
|
+
return false;
|
|
52
|
+
const posix = toProjectRelativePosix(projectRoot, targetPath);
|
|
53
|
+
// resolve()+relative() collapses mid-path `..`; only outside-root `..` remains.
|
|
54
|
+
if (posix.startsWith(".."))
|
|
55
|
+
return false;
|
|
56
|
+
const base = posix.includes("/") ? posix.slice(posix.lastIndexOf("/") + 1) : posix;
|
|
57
|
+
if (!hasArtifactSuffix(base))
|
|
58
|
+
return false;
|
|
59
|
+
return posix.startsWith("xbrief/proposed/") || posix.startsWith("vbrief/proposed/");
|
|
60
|
+
}
|
|
27
61
|
export function projectRootFromHookPayload(payload, fallback) {
|
|
28
62
|
const input = record(payload);
|
|
29
63
|
if (input === null)
|
|
@@ -124,6 +158,20 @@ export function decideHook(input, seams = {}) {
|
|
|
124
158
|
"Recovery: run `deft session:start`, then " +
|
|
125
159
|
"`deft verify:session-ritual -- --tier=gated`.");
|
|
126
160
|
}
|
|
161
|
+
const writeTarget = hookWriteTargetPath(input.payload);
|
|
162
|
+
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
163
|
+
return {
|
|
164
|
+
verdict: "allow",
|
|
165
|
+
code: "write-propose-ready",
|
|
166
|
+
event: input.event,
|
|
167
|
+
host: input.host,
|
|
168
|
+
toolName,
|
|
169
|
+
projectRoot,
|
|
170
|
+
message: `Directive write gate allowed ${toolName} for a proposed lifecycle xBRIEF ` +
|
|
171
|
+
"(planning write; active scope not required).",
|
|
172
|
+
scopePath: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
127
175
|
let scope;
|
|
128
176
|
try {
|
|
129
177
|
scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
|
|
@@ -132,8 +180,15 @@ export function decideHook(input, seams = {}) {
|
|
|
132
180
|
scope = { ready: false, path: null, message: String(cause) };
|
|
133
181
|
}
|
|
134
182
|
if (!scope.ready) {
|
|
135
|
-
|
|
136
|
-
|
|
183
|
+
const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
184
|
+
const proposedPathHint = relTarget !== null &&
|
|
185
|
+
(relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
|
|
186
|
+
? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
|
|
187
|
+
"filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
|
|
188
|
+
"planning writes (#2625)."
|
|
189
|
+
: " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
|
|
190
|
+
"or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption).";
|
|
191
|
+
return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
|
|
137
192
|
}
|
|
138
193
|
return {
|
|
139
194
|
verdict: "allow",
|
|
@@ -150,22 +205,24 @@ export function decideHook(input, seams = {}) {
|
|
|
150
205
|
export function renderHostDecision(host, decision) {
|
|
151
206
|
if (decision.verdict === "allow")
|
|
152
207
|
return "";
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
switch (host) {
|
|
209
|
+
case "claude":
|
|
210
|
+
case "codex":
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
hookSpecificOutput: {
|
|
213
|
+
hookEventName: "PreToolUse",
|
|
214
|
+
permissionDecision: "deny",
|
|
215
|
+
permissionDecisionReason: decision.message,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
case "grok":
|
|
219
|
+
return JSON.stringify({ decision: "deny", reason: decision.message });
|
|
220
|
+
case "cursor":
|
|
221
|
+
return JSON.stringify({
|
|
222
|
+
permission: "deny",
|
|
223
|
+
user_message: decision.message,
|
|
224
|
+
agent_message: decision.message,
|
|
225
|
+
});
|
|
164
226
|
}
|
|
165
|
-
return JSON.stringify({
|
|
166
|
-
permission: "deny",
|
|
167
|
-
user_message: decision.message,
|
|
168
|
-
agent_message: decision.message,
|
|
169
|
-
});
|
|
170
227
|
}
|
|
171
228
|
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -2,7 +2,7 @@ import type { HookHost } from "../hooks/dispatcher.js";
|
|
|
2
2
|
import type { InitDepositIo } from "./constants.js";
|
|
3
3
|
export declare const DIRECT_WRITE_HOOK_MATCHER: string;
|
|
4
4
|
export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
|
|
5
|
-
export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json"];
|
|
5
|
+
export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
|
|
6
6
|
export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
|
|
7
7
|
export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
|
|
8
8
|
export interface AgentHookInspection {
|
|
@@ -8,6 +8,7 @@ export const AGENT_HOOK_PATHS = [
|
|
|
8
8
|
".claude/settings.json",
|
|
9
9
|
".grok/hooks/deft.json",
|
|
10
10
|
".cursor/hooks.json",
|
|
11
|
+
".codex/hooks.json",
|
|
11
12
|
];
|
|
12
13
|
function object(value) {
|
|
13
14
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -125,6 +126,10 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
|
|
|
125
126
|
merge: (config, path) => mergeNestedConfig(config, path, "grok"),
|
|
126
127
|
},
|
|
127
128
|
{ path: AGENT_HOOK_PATHS[2], merge: mergeCursorConfig },
|
|
129
|
+
{
|
|
130
|
+
path: AGENT_HOOK_PATHS[3],
|
|
131
|
+
merge: (config, path) => mergeNestedConfig(config, path, "codex"),
|
|
132
|
+
},
|
|
128
133
|
];
|
|
129
134
|
const prepared = definitions.map((definition) => {
|
|
130
135
|
const absolute = join(projectRoot, definition.path);
|
|
@@ -187,6 +192,11 @@ export function inspectAgentHookDeposit(projectRoot) {
|
|
|
187
192
|
valid: (config) => hasNestedRegistration(config, "grok"),
|
|
188
193
|
},
|
|
189
194
|
{ host: "cursor", path: AGENT_HOOK_PATHS[2], valid: hasCursorRegistration },
|
|
195
|
+
{
|
|
196
|
+
host: "codex",
|
|
197
|
+
path: AGENT_HOOK_PATHS[3],
|
|
198
|
+
valid: (config) => hasNestedRegistration(config, "codex"),
|
|
199
|
+
},
|
|
190
200
|
];
|
|
191
201
|
return definitions.map((definition) => {
|
|
192
202
|
const absolute = join(projectRoot, definition.path);
|
|
@@ -24,6 +24,7 @@ export function installerManagedMatchers() {
|
|
|
24
24
|
{ exact: ".claude/settings.json" },
|
|
25
25
|
{ exact: ".grok/hooks/deft.json" },
|
|
26
26
|
{ exact: ".cursor/hooks.json" },
|
|
27
|
+
{ exact: ".codex/hooks.json" },
|
|
27
28
|
{ exact: ".gitattributes" },
|
|
28
29
|
{ exact: ".gitignore" },
|
|
29
30
|
{ exact: "greptile.json" },
|
|
@@ -18,6 +18,7 @@ import { ensureInitGitignoreLines, reconstituteDepositFromContent } from "./giti
|
|
|
18
18
|
import { depositStagePaths, printCommitGuidance } from "./hygiene.js";
|
|
19
19
|
import { buildLegacyRefusalJson, buildLegacyRefusalMessage, detectLegacyLayout, LEGACY_LAYOUT_REFUSED_EXIT_CODE, LegacyLayoutRefusedError, } from "./legacy-detect.js";
|
|
20
20
|
import { printMigrateNudgeIfNeeded } from "./migrate.js";
|
|
21
|
+
import { ensurePrettierIgnoreLines } from "./prettierignore.js";
|
|
21
22
|
import { CANONICAL_INSTALL_ROOT, depositNeutralization, ensureTaskfile, writeAgentsMd, writeAgentsSkills, writeConsumerGitHooks, writeConsumerVbrief, writeInstallManifest, } from "./scaffold.js";
|
|
22
23
|
export function parseInitArgv(canonicalArgv, userArgv = []) {
|
|
23
24
|
const args = [...canonicalArgv, ...userArgv];
|
|
@@ -137,6 +138,7 @@ export async function runInitDeposit(args, io, seams = {}) {
|
|
|
137
138
|
await reconstituteDepositFromContent(contentRoot, deftDir, copyContent);
|
|
138
139
|
await prunePythonArtifactsFromDeposit(deftDir, projectDir, io);
|
|
139
140
|
ensureInitGitignoreLines(projectDir, io);
|
|
141
|
+
ensurePrettierIgnoreLines(projectDir, io);
|
|
140
142
|
const nowIso = seams.nowIso ?? (() => new Date().toISOString().replace(/\.\d{3}Z$/, "Z"));
|
|
141
143
|
const version = readContentVersion(contentRoot, seams.readPackageVersion ?? readCorePackageVersion);
|
|
142
144
|
const manifestFields = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer `.prettierignore` upkeep for install and npm-managed update (#2534).
|
|
3
|
+
*
|
|
4
|
+
* Managed `.deft/core/` is outside the consumer Prettier format gate. Official
|
|
5
|
+
* install and `directive update` heal the ignore entry idempotently without
|
|
6
|
+
* reformatting the vendored deposit.
|
|
7
|
+
*
|
|
8
|
+
* Refs #2534, #670.
|
|
9
|
+
*/
|
|
10
|
+
import type { InitDepositIo } from "./constants.js";
|
|
11
|
+
/** Directory ignore entry for the hybrid deposit. */
|
|
12
|
+
export declare const PRETTIERIGNORE_DEFT_CORE_LINE = ".deft/core/";
|
|
13
|
+
export interface EnsurePrettierIgnoreResult {
|
|
14
|
+
readonly changed: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ensure the consumer `.prettierignore` excludes managed `.deft/core/` from
|
|
18
|
+
* Prettier. Idempotent; preserves consumer preamble.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ensurePrettierIgnoreLines(projectDir: string, io: InitDepositIo): EnsurePrettierIgnoreResult;
|
|
21
|
+
//# sourceMappingURL=prettierignore.d.ts.map
|