@deftai/directive-core 0.79.1 → 0.79.3
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.js +14 -4
- package/dist/capacity/backfill.js +4 -2
- package/dist/category-b-namespace/index.js +31 -6
- package/dist/check/orchestrator.d.ts +4 -0
- package/dist/check/orchestrator.js +9 -1
- package/dist/content-contracts/skills/skill-frontmatter.js +6 -2
- package/dist/eval/health.js +8 -1
- package/dist/eval/run.js +2 -0
- package/dist/fs/projection-containment.d.ts +11 -0
- package/dist/fs/projection-containment.js +62 -1
- package/dist/hooks/dispatcher.d.ts +1 -1
- package/dist/hooks/dispatcher.js +66 -7
- package/dist/init-deposit/hygiene.js +3 -0
- package/dist/intake/reconcile-issues.d.ts +1 -1
- package/dist/intake/reconcile-issues.js +25 -2
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +1 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +3 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +4 -0
- package/dist/issue-sync/sync-from-xbrief.js +12 -0
- package/dist/lifecycle/event.d.ts +15 -0
- package/dist/lifecycle/event.js +163 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/orchestration/verify-judgment-gates.js +18 -2
- package/dist/platform/resolve-version.d.ts +2 -6
- package/dist/platform/resolve-version.js +3 -129
- package/dist/pr-merge-readiness/compute.d.ts +2 -1
- package/dist/pr-merge-readiness/compute.js +23 -6
- package/dist/pr-merge-readiness/evaluate.d.ts +2 -1
- package/dist/pr-merge-readiness/evaluate.js +46 -31
- package/dist/pr-merge-readiness/gh.js +13 -5
- package/dist/pr-merge-readiness/greptile-inline.d.ts +27 -0
- package/dist/pr-merge-readiness/greptile-inline.js +239 -0
- package/dist/pr-merge-readiness/index.d.ts +1 -0
- package/dist/pr-merge-readiness/index.js +1 -0
- package/dist/pr-merge-readiness/mergeability.d.ts +2 -1
- package/dist/pr-merge-readiness/mergeability.js +7 -1
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
- package/dist/pr-monitor/monitor.js +10 -1
- package/dist/pr-watch/constants.d.ts +5 -0
- package/dist/pr-watch/constants.js +27 -0
- package/dist/pr-watch/index.d.ts +1 -1
- package/dist/pr-watch/index.js +1 -1
- package/dist/pr-watch/main.d.ts +3 -0
- package/dist/pr-watch/main.js +29 -3
- package/dist/release/constants.d.ts +5 -0
- package/dist/release/constants.js +14 -2
- package/dist/release/flags.js +16 -0
- package/dist/release/main.js +7 -0
- package/dist/release/pipeline.js +7 -3
- package/dist/release/preflight.js +8 -1
- package/dist/release/skip-ci-incident.d.ts +22 -0
- package/dist/release/skip-ci-incident.js +68 -0
- package/dist/release/types.d.ts +2 -0
- package/dist/release/version.js +68 -32
- package/dist/release-e2e/entrypoint-worker.js +1 -0
- package/dist/release-e2e/entrypoint.js +13 -0
- package/dist/review-monitor/constants.d.ts +14 -0
- package/dist/review-monitor/constants.js +55 -0
- package/dist/review-monitor/index.d.ts +5 -0
- package/dist/review-monitor/index.js +5 -0
- package/dist/review-monitor/record.d.ts +50 -0
- package/dist/review-monitor/record.js +178 -0
- package/dist/review-monitor/tier-detection.d.ts +15 -0
- package/dist/review-monitor/tier-detection.js +61 -0
- package/dist/review-monitor/verify.d.ts +32 -0
- package/dist/review-monitor/verify.js +171 -0
- package/dist/scope/demote.js +20 -0
- package/dist/swarm/worktrees.d.ts +3 -0
- package/dist/swarm/worktrees.js +18 -2
- package/dist/task-surface/index.js +3 -2
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +16 -8
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +41 -14
- package/dist/xbrief-migrate/migration-containment.js +9 -25
- package/package.json +7 -3
package/dist/cache/fetch.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
3
4
|
import { GhRestError, InvalidRepoError, restIssueView, runGhApi, } from "../scm/gh-rest.js";
|
|
4
5
|
import { DEFAULT_BATCH_SIZE, DEFAULT_DELAY_MS } from "./constants.js";
|
|
5
6
|
import { CacheError, CacheFetchError } from "./errors.js";
|
|
@@ -569,8 +570,9 @@ function readSelfHealState(cacheRoot) {
|
|
|
569
570
|
return null;
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
|
-
function writeSelfHealState(cacheRoot, when) {
|
|
573
|
-
const path = join(cacheRoot, SELF_HEAL_STATE_FILENAME);
|
|
573
|
+
function writeSelfHealState(projectRoot, cacheRoot, when) {
|
|
574
|
+
const path = join(resolve(cacheRoot), SELF_HEAL_STATE_FILENAME);
|
|
575
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
574
576
|
writeFileSync(path, `${JSON.stringify({ last_reconcile_at: when.toISOString() })}\n`, "utf8");
|
|
575
577
|
}
|
|
576
578
|
/** TTL-bounded closed-reconcile for session ritual / triage:welcome self-healing (#1886). */
|
|
@@ -623,7 +625,15 @@ export function maybeSelfHealCache(projectRoot, options = {}) {
|
|
|
623
625
|
try {
|
|
624
626
|
const refresh = refreshFn({ source, repo, cacheRoot, openNumbers });
|
|
625
627
|
if (options.writeState !== false && refresh.refreshFailed === 0) {
|
|
626
|
-
|
|
628
|
+
try {
|
|
629
|
+
writeSelfHealState(projectRoot, cacheRoot, now);
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
if (err instanceof ProjectionContainmentError) {
|
|
633
|
+
return { skipped: true, skipReason: "containment-refused", drift, refresh: null };
|
|
634
|
+
}
|
|
635
|
+
throw err;
|
|
636
|
+
}
|
|
627
637
|
}
|
|
628
638
|
return { skipped: false, skipReason: null, drift, refresh };
|
|
629
639
|
}
|
|
@@ -3,6 +3,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
5
5
|
import { sortedStringifyCompact } from "../codebase/json.js";
|
|
6
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
6
7
|
import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
|
|
7
8
|
import { classifyBucket, loadBucketMatchers, resolveCapacityAllocation, SOURCE_MATCH, } from "../policy/capacity.js";
|
|
8
9
|
export const COMPLETED_FOLDER = "completed";
|
|
@@ -137,7 +138,8 @@ function inWindow(completedAt, windowDays, now) {
|
|
|
137
138
|
const ageDays = (now.getTime() - parsed.getTime()) / (86400 * 1000);
|
|
138
139
|
return ageDays >= 0 && ageDays <= windowDays;
|
|
139
140
|
}
|
|
140
|
-
function writeMetadata(path, data, plan, metadata, options) {
|
|
141
|
+
function writeMetadata(projectRoot, path, data, plan, metadata, options) {
|
|
142
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
141
143
|
if (typeof plan.metadata !== "object" || plan.metadata === null || Array.isArray(plan.metadata)) {
|
|
142
144
|
plan.metadata = metadata;
|
|
143
145
|
}
|
|
@@ -272,7 +274,7 @@ export async function backfill(projectRoot, options = {}) {
|
|
|
272
274
|
result.items.push(item);
|
|
273
275
|
if (!result.dry_run && (setBucket || setCompletedAt)) {
|
|
274
276
|
try {
|
|
275
|
-
writeMetadata(path, data, planRec, metadata, {
|
|
277
|
+
writeMetadata(projectRoot, path, data, planRec, metadata, {
|
|
276
278
|
bucket: setBucket ? bucket : undefined,
|
|
277
279
|
source: setBucket ? source : undefined,
|
|
278
280
|
completedAt: setCompletedAt ? (gitCompletedAt ?? undefined) : undefined,
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* order-preserving (the namespaced key takes the legacy key's slot, keeping
|
|
10
10
|
* artifact diffs minimal).
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, readdirSync, readFileSync,
|
|
12
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
13
13
|
import { join, relative, resolve } from "node:path";
|
|
14
|
+
import { assertDirectoryNotSymlink, assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
14
15
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
15
16
|
import { LEGACY_PLAN_COMPLETED_NOTE_KEY, LEGACY_PLAN_POLICY_KEY, PLAN_COMPLETED_NOTE_KEY, PLAN_POLICY_KEY, } from "../policy/plan-extensions.js";
|
|
16
17
|
/** The locked Category B renames (#1650): directive's own config, not xbrief data. */
|
|
@@ -69,10 +70,20 @@ function collectVbriefFiles(dir, acc = []) {
|
|
|
69
70
|
}
|
|
70
71
|
for (const entry of entries) {
|
|
71
72
|
const full = join(dir, entry.name);
|
|
72
|
-
|
|
73
|
+
let info;
|
|
74
|
+
try {
|
|
75
|
+
info = lstatSync(full);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (info.isSymbolicLink()) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (info.isDirectory()) {
|
|
73
84
|
collectVbriefFiles(full, acc);
|
|
74
85
|
}
|
|
75
|
-
else if (
|
|
86
|
+
else if (info.isFile() && hasArtifactSuffix(entry.name)) {
|
|
76
87
|
acc.push(full);
|
|
77
88
|
}
|
|
78
89
|
}
|
|
@@ -100,13 +111,17 @@ export function migrateCategoryBCorpus(projectRoot) {
|
|
|
100
111
|
return { scanned: 0, changed: [], conflicts: [] };
|
|
101
112
|
}
|
|
102
113
|
}
|
|
114
|
+
try {
|
|
115
|
+
assertDirectoryNotSymlink(root, vbriefDir, "lifecycle root");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
+
return { scanned: 0, changed: [], conflicts: [{ path: relative(root, vbriefDir), message }] };
|
|
120
|
+
}
|
|
103
121
|
let scanned = 0;
|
|
104
122
|
const changed = [];
|
|
105
123
|
const conflicts = [];
|
|
106
124
|
for (const file of collectVbriefFiles(vbriefDir)) {
|
|
107
|
-
if (!statSync(file).isFile()) {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
125
|
scanned += 1;
|
|
111
126
|
let parsed;
|
|
112
127
|
try {
|
|
@@ -125,6 +140,16 @@ export function migrateCategoryBCorpus(projectRoot) {
|
|
|
125
140
|
continue;
|
|
126
141
|
}
|
|
127
142
|
if (result.changed) {
|
|
143
|
+
try {
|
|
144
|
+
assertWriteTargetSafe(root, file);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
conflicts.push({
|
|
148
|
+
path: relPath,
|
|
149
|
+
message: err instanceof Error ? err.message : String(err),
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
128
153
|
writeFileSync(file, `${JSON.stringify(result.doc, null, 2)}\n`, "utf8");
|
|
129
154
|
changed.push(relPath);
|
|
130
155
|
}
|
|
@@ -20,12 +20,16 @@ export interface CheckOrchestratorSeams {
|
|
|
20
20
|
cwd: string;
|
|
21
21
|
stdio: string;
|
|
22
22
|
env?: NodeJS.ProcessEnv;
|
|
23
|
+
timeoutMs?: number;
|
|
23
24
|
}) => {
|
|
24
25
|
status: number | null;
|
|
26
|
+
signal?: NodeJS.Signals | null;
|
|
25
27
|
error?: Error;
|
|
26
28
|
};
|
|
27
29
|
/** Child-process environment (default: process.env). */
|
|
28
30
|
readonly env?: NodeJS.ProcessEnv;
|
|
31
|
+
/** Wall-clock spawn timeout in milliseconds (default: none). */
|
|
32
|
+
readonly timeoutMs?: number;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* True when `path` is the directive framework source checkout root (not a
|
|
@@ -78,11 +78,16 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
|
|
|
78
78
|
cwd,
|
|
79
79
|
stdio: "inherit",
|
|
80
80
|
env: seams.env,
|
|
81
|
+
timeoutMs: seams.timeoutMs,
|
|
81
82
|
});
|
|
82
83
|
if (result.error !== undefined) {
|
|
83
84
|
process.stderr.write(`check: failed to invoke task: ${result.error.message}\n`);
|
|
84
85
|
return 2;
|
|
85
86
|
}
|
|
87
|
+
if (result.signal === "SIGTERM" && result.status === null && seams.timeoutMs !== undefined) {
|
|
88
|
+
process.stderr.write(`check: timed out after ${seams.timeoutMs / 60_000}m (Step 5 vitest coverage budget; #2652)\n`);
|
|
89
|
+
return 124;
|
|
90
|
+
}
|
|
86
91
|
return result.status ?? 1;
|
|
87
92
|
}
|
|
88
93
|
function defaultSpawn(cmd, args, opts) {
|
|
@@ -90,7 +95,10 @@ function defaultSpawn(cmd, args, opts) {
|
|
|
90
95
|
cwd: opts.cwd,
|
|
91
96
|
stdio: opts.stdio,
|
|
92
97
|
env: opts.env ?? process.env,
|
|
98
|
+
...(opts.timeoutMs !== undefined
|
|
99
|
+
? { timeout: opts.timeoutMs, killSignal: "SIGTERM" }
|
|
100
|
+
: {}),
|
|
93
101
|
});
|
|
94
|
-
return { status: result.status, error: result.error };
|
|
102
|
+
return { status: result.status, signal: result.signal, error: result.error };
|
|
95
103
|
}
|
|
96
104
|
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -34,8 +34,12 @@ function parseOsArray(fm) {
|
|
|
34
34
|
const body = match.groups?.body ?? "";
|
|
35
35
|
return [...body.matchAll(OS_TOKEN)].map((m) => m[1] ?? "");
|
|
36
36
|
}
|
|
37
|
-
function pathHasExcludedPart(filePath) {
|
|
38
|
-
const
|
|
37
|
+
function pathHasExcludedPart(filePath, repoRoot = REPO_ROOT) {
|
|
38
|
+
const rel = relative(repoRoot, filePath).replace(/\\/g, "/");
|
|
39
|
+
if (rel.startsWith("..") || rel.length === 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
const parts = rel.split("/");
|
|
39
43
|
return parts.some((part) => EXCLUDED_PARTS.has(part));
|
|
40
44
|
}
|
|
41
45
|
function discoverSkillMdFiles(dir, results) {
|
package/dist/eval/health.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import { agentsRefreshPlan } from "../doctor/agents-md.js";
|
|
4
4
|
import { evaluate as evaluateEncoding } from "../encoding/evaluate.js";
|
|
5
5
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
6
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
6
7
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
7
8
|
import { healthMetricsHistoryPath } from "../metrics/resolve-metrics-home.js";
|
|
8
9
|
import { readPlanPolicy } from "../policy/plan-extensions.js";
|
|
@@ -162,6 +163,12 @@ export function persistHealthRun(projectRoot, report) {
|
|
|
162
163
|
if (path === null) {
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
166
|
+
const projectAbs = resolve(projectRoot);
|
|
167
|
+
const targetAbs = resolve(path);
|
|
168
|
+
const rel = relative(projectAbs, targetAbs);
|
|
169
|
+
if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
|
|
170
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
171
|
+
}
|
|
165
172
|
mkdirSync(dirname(path), { recursive: true });
|
|
166
173
|
appendFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
|
|
167
174
|
}
|
package/dist/eval/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { appendFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "n
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { readCorePackageVersion } from "../engine-version.js";
|
|
6
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
6
7
|
import { resolveEvalPath } from "../layout/resolve.js";
|
|
7
8
|
import { BYTE_DIFF_WHOLE_FILE_THRESHOLD, InstrumentedVbriefCrud } from "./crud-telemetry.js";
|
|
8
9
|
import { evaluateHealth } from "./health.js";
|
|
@@ -194,6 +195,7 @@ export function goldenRunsHistoryPath(projectRoot) {
|
|
|
194
195
|
/** Append one golden run to the versioned ledger (#1703 Tier 2). */
|
|
195
196
|
export function persistGoldenRun(projectRoot, record) {
|
|
196
197
|
const path = goldenRunsHistoryPath(projectRoot);
|
|
198
|
+
assertWriteTargetSafe(projectRoot, path);
|
|
197
199
|
mkdirSync(dirname(path), { recursive: true });
|
|
198
200
|
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
199
201
|
}
|
|
@@ -32,4 +32,15 @@ export declare class ProjectionContainmentError extends Error {
|
|
|
32
32
|
* MUST be called BEFORE any projection read/write/mkdir.
|
|
33
33
|
*/
|
|
34
34
|
export declare function assertProjectionContained(projectDir: string, targetPath: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Refuse writes when the resolved target already exists as a symlink (#2626 / #2632).
|
|
37
|
+
* Pair with {@link assertProjectionContained} before read/write/mkdir/append.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertWriteTargetSafe(projectDir: string, targetPath: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Refuse when a lifecycle corpus root is itself a symlink (#2626 category-b).
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertDirectoryNotSymlink(projectDir: string, dirPath: string, label: string): void;
|
|
44
|
+
/** Walk a directory tree and refuse any symlink entry (#2601 / #2626). */
|
|
45
|
+
export declare function walkDirectoryRejectSymlinks(root: string): void;
|
|
35
46
|
//# sourceMappingURL=projection-containment.d.ts.map
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Refs #2413.
|
|
15
15
|
*/
|
|
16
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
16
|
+
import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
17
17
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
18
18
|
/** Non-zero exit code for a projection-containment refusal (needs-action). */
|
|
19
19
|
export const PROJECTION_CONTAINMENT_REFUSED_EXIT_CODE = 2;
|
|
@@ -115,4 +115,65 @@ export function assertProjectionContained(projectDir, targetPath) {
|
|
|
115
115
|
`(${deepestExistingReal} is outside ${projectReal})`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: deepestExistingReal });
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Refuse writes when the resolved target already exists as a symlink (#2626 / #2632).
|
|
120
|
+
* Pair with {@link assertProjectionContained} before read/write/mkdir/append.
|
|
121
|
+
*/
|
|
122
|
+
export function assertWriteTargetSafe(projectDir, targetPath) {
|
|
123
|
+
assertProjectionContained(projectDir, targetPath);
|
|
124
|
+
const targetAbs = resolve(targetPath);
|
|
125
|
+
let info;
|
|
126
|
+
try {
|
|
127
|
+
info = lstatSync(targetAbs);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (info.isSymbolicLink()) {
|
|
133
|
+
throw new ProjectionContainmentError(`projection write refused: ${targetAbs} is a symlink on the write path`, { projectDir: resolve(projectDir), targetPath: targetAbs, offendingPath: targetAbs });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Refuse when a lifecycle corpus root is itself a symlink (#2626 category-b).
|
|
138
|
+
*/
|
|
139
|
+
export function assertDirectoryNotSymlink(projectDir, dirPath, label) {
|
|
140
|
+
assertProjectionContained(projectDir, dirPath);
|
|
141
|
+
const dirAbs = resolve(dirPath);
|
|
142
|
+
let info;
|
|
143
|
+
try {
|
|
144
|
+
info = lstatSync(dirAbs);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new ProjectionContainmentError(`projection write refused: ${label} ${dirAbs} does not exist`, { projectDir: resolve(projectDir), targetPath: dirAbs, offendingPath: dirAbs });
|
|
148
|
+
}
|
|
149
|
+
if (info.isSymbolicLink()) {
|
|
150
|
+
throw new ProjectionContainmentError(`projection write refused: ${label} ${dirAbs} must not be a symlink`, { projectDir: resolve(projectDir), targetPath: dirAbs, offendingPath: dirAbs });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Walk a directory tree and refuse any symlink entry (#2601 / #2626). */
|
|
154
|
+
export function walkDirectoryRejectSymlinks(root) {
|
|
155
|
+
let entries;
|
|
156
|
+
try {
|
|
157
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const full = join(root, entry.name);
|
|
164
|
+
let info;
|
|
165
|
+
try {
|
|
166
|
+
info = lstatSync(full);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (info.isSymbolicLink()) {
|
|
172
|
+
throw new ProjectionContainmentError(`projection write refused: symlink on traversal path: ${full}`, { projectDir: resolve(root), targetPath: full, offendingPath: full });
|
|
173
|
+
}
|
|
174
|
+
if (info.isDirectory()) {
|
|
175
|
+
walkDirectoryRejectSymlinks(full);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
118
179
|
//# sourceMappingURL=projection-containment.js.map
|
|
@@ -32,7 +32,7 @@ export interface HookPolicySeams {
|
|
|
32
32
|
stderr: string;
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
export declare function hookToolName(payload: unknown): string | null;
|
|
35
|
+
export declare function hookToolName(payload: unknown, host?: HookHost): string | null;
|
|
36
36
|
/**
|
|
37
37
|
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
38
38
|
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -19,11 +19,70 @@ function firstString(...values) {
|
|
|
19
19
|
}
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
function fieldPresent(input, key) {
|
|
23
|
+
return key in input;
|
|
24
|
+
}
|
|
25
|
+
function fieldString(input, key) {
|
|
26
|
+
const value = input[key];
|
|
27
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
28
|
+
return value.trim();
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function toolInputRecord(payload) {
|
|
32
|
+
return record(payload.tool_input) ?? record(payload.toolInput) ?? record(payload.input);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
|
|
36
|
+
* fired for a direct-write tool (#2628). Infer from nested tool input when possible.
|
|
37
|
+
*/
|
|
38
|
+
function inferCursorDirectWriteToolName(payload) {
|
|
39
|
+
const toolInput = toolInputRecord(payload);
|
|
40
|
+
if (toolInput !== null) {
|
|
41
|
+
if (fieldPresent(toolInput, "new_string") ||
|
|
42
|
+
fieldPresent(toolInput, "newString") ||
|
|
43
|
+
fieldPresent(toolInput, "old_string") ||
|
|
44
|
+
fieldPresent(toolInput, "oldString")) {
|
|
45
|
+
return "StrReplace";
|
|
46
|
+
}
|
|
47
|
+
if (fieldString(toolInput, "contents") !== null ||
|
|
48
|
+
fieldString(toolInput, "content") !== null ||
|
|
49
|
+
fieldString(toolInput, "text") !== null) {
|
|
50
|
+
return "Write";
|
|
51
|
+
}
|
|
52
|
+
if (fieldString(toolInput, "patch") !== null ||
|
|
53
|
+
fieldString(toolInput, "unified_diff") !== null ||
|
|
54
|
+
fieldString(toolInput, "diff") !== null) {
|
|
55
|
+
return "ApplyPatch";
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(toolInput.edits))
|
|
58
|
+
return "MultiEdit";
|
|
59
|
+
if (Array.isArray(toolInput.cells) || toolInput.cell_id != null) {
|
|
60
|
+
return "NotebookEdit";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Cursor maps Claude Edit → Write; a write target without contents is still a direct write.
|
|
64
|
+
if (hookWriteTargetPath(payload) !== null)
|
|
65
|
+
return "Write";
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
export function hookToolName(payload, host) {
|
|
23
69
|
const input = record(payload);
|
|
24
70
|
if (input === null)
|
|
25
71
|
return null;
|
|
26
|
-
|
|
72
|
+
const direct = fieldString(input, "tool_name") ?? fieldString(input, "toolName") ?? fieldString(input, "tool");
|
|
73
|
+
if (direct !== null)
|
|
74
|
+
return direct;
|
|
75
|
+
if (host === "cursor")
|
|
76
|
+
return inferCursorDirectWriteToolName(input);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function missingToolNameMessage(host) {
|
|
80
|
+
if (host === "cursor") {
|
|
81
|
+
return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
|
|
82
|
+
"recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
|
|
83
|
+
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
84
|
+
}
|
|
85
|
+
return "Directive denied this matched write event because the host payload omitted its tool name.";
|
|
27
86
|
}
|
|
28
87
|
/**
|
|
29
88
|
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
@@ -33,14 +92,14 @@ export function hookWriteTargetPath(payload) {
|
|
|
33
92
|
const input = record(payload);
|
|
34
93
|
if (input === null)
|
|
35
94
|
return null;
|
|
36
|
-
const toolInput =
|
|
95
|
+
const toolInput = toolInputRecord(input);
|
|
37
96
|
return firstString(toolInput?.file_path, toolInput?.filePath, toolInput?.path, input.file_path, input.filePath, input.path);
|
|
38
97
|
}
|
|
39
98
|
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
40
99
|
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
41
|
-
const abs = resolve(projectRoot, targetPath);
|
|
100
|
+
const abs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
|
|
42
101
|
const rel = relative(resolve(projectRoot), abs);
|
|
43
|
-
return rel.split(sep).join("/");
|
|
102
|
+
return rel.split(sep).join("/").replace(/\\/g, "/");
|
|
44
103
|
}
|
|
45
104
|
/**
|
|
46
105
|
* Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
|
|
@@ -128,9 +187,9 @@ export function decideHook(input, seams = {}) {
|
|
|
128
187
|
scopePath: null,
|
|
129
188
|
};
|
|
130
189
|
}
|
|
131
|
-
const toolName = hookToolName(input.payload);
|
|
190
|
+
const toolName = hookToolName(input.payload, input.host);
|
|
132
191
|
if (toolName === null) {
|
|
133
|
-
return deny(input, "invalid-input", null,
|
|
192
|
+
return deny(input, "invalid-input", null, missingToolNameMessage(input.host));
|
|
134
193
|
}
|
|
135
194
|
if (!isDirectWriteTool(toolName)) {
|
|
136
195
|
return {
|
|
@@ -27,6 +27,9 @@ export function installerManagedMatchers() {
|
|
|
27
27
|
{ exact: ".codex/hooks.json" },
|
|
28
28
|
{ exact: ".gitattributes" },
|
|
29
29
|
{ exact: ".gitignore" },
|
|
30
|
+
// Installer-deposited Prettier gate exclusion (#2534); must be allowlisted or
|
|
31
|
+
// framework-only deposit PRs trip no-mixed-core-and-app (#2629).
|
|
32
|
+
{ exact: ".prettierignore" },
|
|
30
33
|
{ exact: "greptile.json" },
|
|
31
34
|
{ exact: CODEQL_CONFIG_REL },
|
|
32
35
|
{ exact: CORE_GUARD_WORKFLOW_REL },
|
|
@@ -56,7 +56,7 @@ export declare function formatJson(report: ReconcileReport): string;
|
|
|
56
56
|
export declare function formatMarkdown(report: ReconcileReport): string;
|
|
57
57
|
export declare function applyLifecycleFixes(vbriefDir: string, report: ReconcileReport, projectRoot?: string | null): [number, number, string[]];
|
|
58
58
|
/** Repair completed/ files whose plan.status is not terminal (#2578). */
|
|
59
|
-
export declare function repairCompletedStatusDrift(vbriefDir: string, drift: readonly CompletedStatusDriftEntry[]): [number, number, string[]];
|
|
59
|
+
export declare function repairCompletedStatusDrift(vbriefDir: string, drift: readonly CompletedStatusDriftEntry[], projectRoot?: string | null): [number, number, string[]];
|
|
60
60
|
export declare function detectRepo(): string | null;
|
|
61
61
|
export interface ReconcileCliArgs {
|
|
62
62
|
vbriefDir?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
6
|
import { call } from "../scm/call.js";
|
|
6
7
|
import { updateDecomposedChildBackReferences } from "../scope/decomposed-refs.js";
|
|
@@ -673,6 +674,17 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
|
|
|
673
674
|
failures.push(`vBRIEF file missing: ${relPath}`);
|
|
674
675
|
continue;
|
|
675
676
|
}
|
|
677
|
+
try {
|
|
678
|
+
assertWriteTargetSafe(cwd, src);
|
|
679
|
+
assertProjectionContained(cwd, join(vbriefDir, destFolder));
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
if (err instanceof ProjectionContainmentError) {
|
|
683
|
+
failures.push(`${relPath}: ${err.message}`);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
676
688
|
let data;
|
|
677
689
|
try {
|
|
678
690
|
data = JSON.parse(readFileSync(src, "utf8"));
|
|
@@ -728,11 +740,12 @@ export function applyLifecycleFixes(vbriefDir, report, projectRoot = null) {
|
|
|
728
740
|
return [moved, skipped, failures];
|
|
729
741
|
}
|
|
730
742
|
/** Repair completed/ files whose plan.status is not terminal (#2578). */
|
|
731
|
-
export function repairCompletedStatusDrift(vbriefDir, drift) {
|
|
743
|
+
export function repairCompletedStatusDrift(vbriefDir, drift, projectRoot = null) {
|
|
732
744
|
let repaired = 0;
|
|
733
745
|
let skipped = 0;
|
|
734
746
|
const failures = [];
|
|
735
747
|
const completedDir = join(vbriefDir, "completed");
|
|
748
|
+
const cwd = projectRoot ?? resolve(vbriefDir, "..");
|
|
736
749
|
for (const entry of drift) {
|
|
737
750
|
const slash = entry.rel_path.indexOf("/");
|
|
738
751
|
if (slash < 0) {
|
|
@@ -741,6 +754,16 @@ export function repairCompletedStatusDrift(vbriefDir, drift) {
|
|
|
741
754
|
}
|
|
742
755
|
const filename = entry.rel_path.slice(slash + 1);
|
|
743
756
|
const src = join(completedDir, filename);
|
|
757
|
+
try {
|
|
758
|
+
assertWriteTargetSafe(cwd, src);
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
if (err instanceof ProjectionContainmentError) {
|
|
762
|
+
failures.push(`${entry.rel_path}: ${err.message}`);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
744
767
|
let data;
|
|
745
768
|
try {
|
|
746
769
|
data = JSON.parse(readFileSync(src, "utf8"));
|
|
@@ -876,7 +899,7 @@ export function reconcileMain(args) {
|
|
|
876
899
|
}, 0);
|
|
877
900
|
const [moved, skipped, failures] = applyLifecycleFixes(vbriefDir, applyReport, projectRoot);
|
|
878
901
|
const driftAfterMove = scanCompletedStatusDrift(vbriefDir);
|
|
879
|
-
const [driftRepaired, driftSkipped, driftFailures] = repairCompletedStatusDrift(vbriefDir, driftAfterMove);
|
|
902
|
+
const [driftRepaired, driftSkipped, driftFailures] = repairCompletedStatusDrift(vbriefDir, driftAfterMove, projectRoot);
|
|
880
903
|
const allFailures = [...failures, ...driftFailures];
|
|
881
904
|
process.stderr.write(`[${moved}/${candidates}] vBRIEFs reconciled (moved=${moved}, already-terminal=${skipped}, failures=${failures.length})\n`);
|
|
882
905
|
process.stderr.write(`[${driftRepaired}/${completedDrift.length}] completed/ status drift repaired (repaired=${driftRepaired}, already-terminal=${driftSkipped}, failures=${driftFailures.length})\n`);
|
|
@@ -8,6 +8,9 @@ export function parseArgs(argv) {
|
|
|
8
8
|
if (arg === "--dry-run") {
|
|
9
9
|
out.dryRun = true;
|
|
10
10
|
}
|
|
11
|
+
else if (arg === "--allow-cross-repo") {
|
|
12
|
+
out.allowCrossRepo = true;
|
|
13
|
+
}
|
|
11
14
|
else if (arg === "--project-root") {
|
|
12
15
|
const value = argv[i + 1];
|
|
13
16
|
if (value === undefined) {
|
|
@@ -29,6 +29,8 @@ export interface SyncFromXbriefOptions {
|
|
|
29
29
|
readonly dryRun?: boolean;
|
|
30
30
|
readonly projectRoot?: string;
|
|
31
31
|
readonly repo?: string;
|
|
32
|
+
readonly allowCrossRepo?: boolean;
|
|
33
|
+
readonly repoAllowlist?: readonly string[];
|
|
32
34
|
readonly runFn?: RunGhApiFn;
|
|
33
35
|
readonly writeErr?: (message: string) => void;
|
|
34
36
|
readonly writeOut?: (message: string) => void;
|
|
@@ -41,6 +43,8 @@ export interface SyncFromXbriefCliArgs {
|
|
|
41
43
|
readonly dryRun?: boolean;
|
|
42
44
|
readonly projectRoot?: string;
|
|
43
45
|
readonly repo?: string;
|
|
46
|
+
readonly allowCrossRepo?: boolean;
|
|
47
|
+
readonly repoAllowlist?: readonly string[];
|
|
44
48
|
}
|
|
45
49
|
export declare function syncFromXbriefMain(args: SyncFromXbriefCliArgs): number;
|
|
46
50
|
//# sourceMappingURL=sync-from-xbrief.d.ts.map
|
|
@@ -6,6 +6,7 @@ import { provenanceIssueNumber, repoSlugFromUrl } from "../intake/issue-ingest.j
|
|
|
6
6
|
import { extractReferencesFromVbrief, GITHUB_ISSUE_REF_TYPES, parseIssueNumber, } from "../intake/reconcile-issues.js";
|
|
7
7
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
8
8
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
9
|
+
import { isRepoMutationAllowed } from "../vbrief-reconcile/repo-guard.js";
|
|
9
10
|
export const SYNC_COMMENT_HEADER = "## xBRIEF sync (deft issue:sync-from-xbrief)";
|
|
10
11
|
export function resolveOriginIssue(data, options = {}) {
|
|
11
12
|
const refs = extractReferencesFromVbrief(data);
|
|
@@ -161,6 +162,15 @@ export function syncFromXbrief(options) {
|
|
|
161
162
|
writeOut(`issue:sync-from-xbrief: no material AC/status changes since last sync for #${origin.number}; nothing to post.`);
|
|
162
163
|
return 0;
|
|
163
164
|
}
|
|
165
|
+
const mutateGate = isRepoMutationAllowed(origin.repo, projectRoot, {
|
|
166
|
+
allowCrossRepo: options.allowCrossRepo,
|
|
167
|
+
allowlist: options.repoAllowlist,
|
|
168
|
+
explicitRepo: fallbackRepo,
|
|
169
|
+
});
|
|
170
|
+
if (!mutateGate.allowed) {
|
|
171
|
+
writeErr(`issue:sync-from-xbrief: ${mutateGate.reason ?? `refusing cross-repo mutation on ${origin.repo}`}`);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
164
174
|
const relPath = options.xbriefPath.replace(/\\/g, "/");
|
|
165
175
|
const comment = buildSyncComment(data, relPath);
|
|
166
176
|
if (options.dryRun) {
|
|
@@ -203,6 +213,8 @@ export function syncFromXbriefMain(args) {
|
|
|
203
213
|
dryRun: args.dryRun,
|
|
204
214
|
projectRoot: args.projectRoot,
|
|
205
215
|
repo: args.repo,
|
|
216
|
+
allowCrossRepo: args.allowCrossRepo,
|
|
217
|
+
repoAllowlist: args.repoAllowlist,
|
|
206
218
|
});
|
|
207
219
|
}
|
|
208
220
|
//# sourceMappingURL=sync-from-xbrief.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface ParsedEmitInvocation {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly payload: Record<string, unknown>;
|
|
4
|
+
readonly log?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Parse `emit <event> ...` argv for lifecycle:event approval recording. */
|
|
7
|
+
export declare function parseEmitInvocation(args: readonly string[]): ParsedEmitInvocation;
|
|
8
|
+
/** CLI entry for `lifecycle:event` / review-cycle approval recorder (#2631). */
|
|
9
|
+
export declare function runLifecycleEvent(argv: readonly string[], options?: {
|
|
10
|
+
projectRoot?: string;
|
|
11
|
+
}): number;
|
|
12
|
+
/** Process entrypoint mirroring other lifecycle CLI modules. */
|
|
13
|
+
export declare function main(argv?: readonly string[]): number;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=event.d.ts.map
|