@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.
Files changed (76) hide show
  1. package/dist/cache/fetch.js +14 -4
  2. package/dist/capacity/backfill.js +4 -2
  3. package/dist/category-b-namespace/index.js +31 -6
  4. package/dist/check/orchestrator.d.ts +4 -0
  5. package/dist/check/orchestrator.js +9 -1
  6. package/dist/content-contracts/skills/skill-frontmatter.js +6 -2
  7. package/dist/eval/health.js +8 -1
  8. package/dist/eval/run.js +2 -0
  9. package/dist/fs/projection-containment.d.ts +11 -0
  10. package/dist/fs/projection-containment.js +62 -1
  11. package/dist/hooks/dispatcher.d.ts +1 -1
  12. package/dist/hooks/dispatcher.js +66 -7
  13. package/dist/init-deposit/hygiene.js +3 -0
  14. package/dist/intake/reconcile-issues.d.ts +1 -1
  15. package/dist/intake/reconcile-issues.js +25 -2
  16. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +1 -0
  17. package/dist/issue-sync/sync-from-xbrief-cli.js +3 -0
  18. package/dist/issue-sync/sync-from-xbrief.d.ts +4 -0
  19. package/dist/issue-sync/sync-from-xbrief.js +12 -0
  20. package/dist/lifecycle/event.d.ts +15 -0
  21. package/dist/lifecycle/event.js +163 -0
  22. package/dist/lifecycle/index.d.ts +1 -0
  23. package/dist/lifecycle/index.js +1 -0
  24. package/dist/orchestration/verify-judgment-gates.js +18 -2
  25. package/dist/platform/resolve-version.d.ts +2 -6
  26. package/dist/platform/resolve-version.js +3 -129
  27. package/dist/pr-merge-readiness/compute.d.ts +2 -1
  28. package/dist/pr-merge-readiness/compute.js +23 -6
  29. package/dist/pr-merge-readiness/evaluate.d.ts +2 -1
  30. package/dist/pr-merge-readiness/evaluate.js +46 -31
  31. package/dist/pr-merge-readiness/gh.js +13 -5
  32. package/dist/pr-merge-readiness/greptile-inline.d.ts +27 -0
  33. package/dist/pr-merge-readiness/greptile-inline.js +239 -0
  34. package/dist/pr-merge-readiness/index.d.ts +1 -0
  35. package/dist/pr-merge-readiness/index.js +1 -0
  36. package/dist/pr-merge-readiness/mergeability.d.ts +2 -1
  37. package/dist/pr-merge-readiness/mergeability.js +7 -1
  38. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
  39. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
  40. package/dist/pr-monitor/monitor.js +10 -1
  41. package/dist/pr-watch/constants.d.ts +5 -0
  42. package/dist/pr-watch/constants.js +27 -0
  43. package/dist/pr-watch/index.d.ts +1 -1
  44. package/dist/pr-watch/index.js +1 -1
  45. package/dist/pr-watch/main.d.ts +3 -0
  46. package/dist/pr-watch/main.js +29 -3
  47. package/dist/release/constants.d.ts +5 -0
  48. package/dist/release/constants.js +14 -2
  49. package/dist/release/flags.js +16 -0
  50. package/dist/release/main.js +7 -0
  51. package/dist/release/pipeline.js +7 -3
  52. package/dist/release/preflight.js +8 -1
  53. package/dist/release/skip-ci-incident.d.ts +22 -0
  54. package/dist/release/skip-ci-incident.js +68 -0
  55. package/dist/release/types.d.ts +2 -0
  56. package/dist/release/version.js +68 -32
  57. package/dist/release-e2e/entrypoint-worker.js +1 -0
  58. package/dist/release-e2e/entrypoint.js +13 -0
  59. package/dist/review-monitor/constants.d.ts +14 -0
  60. package/dist/review-monitor/constants.js +55 -0
  61. package/dist/review-monitor/index.d.ts +5 -0
  62. package/dist/review-monitor/index.js +5 -0
  63. package/dist/review-monitor/record.d.ts +50 -0
  64. package/dist/review-monitor/record.js +178 -0
  65. package/dist/review-monitor/tier-detection.d.ts +15 -0
  66. package/dist/review-monitor/tier-detection.js +61 -0
  67. package/dist/review-monitor/verify.d.ts +32 -0
  68. package/dist/review-monitor/verify.js +171 -0
  69. package/dist/scope/demote.js +20 -0
  70. package/dist/swarm/worktrees.d.ts +3 -0
  71. package/dist/swarm/worktrees.js +18 -2
  72. package/dist/task-surface/index.js +3 -2
  73. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +16 -8
  74. package/dist/vitest-runner/win32-coverage-tmp-setup.js +41 -14
  75. package/dist/xbrief-migrate/migration-containment.js +9 -25
  76. package/package.json +7 -3
@@ -0,0 +1,178 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { closeSync, existsSync, fdatasyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync, } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { DEFAULT_STALE_MINUTES, REVIEW_MONITOR_FILENAME, SCHEMA_VERSION } from "./constants.js";
5
+ function resolveMainWorktreeRoot(startDir) {
6
+ let root = resolve(startDir);
7
+ try {
8
+ const out = execFileSync("git", ["rev-parse", "--git-common-dir"], {
9
+ cwd: startDir,
10
+ encoding: "utf8",
11
+ stdio: ["ignore", "pipe", "ignore"],
12
+ }).trim();
13
+ if (out.length > 0) {
14
+ const commonDir = isAbsolute(out) ? out : resolve(startDir, out);
15
+ root = dirname(commonDir);
16
+ }
17
+ }
18
+ catch {
19
+ // Not a git work tree -- fall back to startDir.
20
+ }
21
+ return root;
22
+ }
23
+ export function reviewMonitorPath(projectRoot) {
24
+ const root = resolveMainWorktreeRoot(projectRoot);
25
+ return join(root, ".deft", REVIEW_MONITOR_FILENAME);
26
+ }
27
+ function atomicWriteJson(path, payload) {
28
+ mkdirSync(dirname(path), { recursive: true });
29
+ const tmp = `${path}.tmp-${process.pid}`;
30
+ const text = `${JSON.stringify(payload, null, 2)}\n`;
31
+ const fd = openSync(tmp, "w");
32
+ try {
33
+ writeSync(fd, text, undefined, "utf8");
34
+ fdatasyncSync(fd);
35
+ }
36
+ finally {
37
+ closeSync(fd);
38
+ }
39
+ renameSync(tmp, path);
40
+ }
41
+ export function emptyReviewMonitorFile() {
42
+ return { schema_version: SCHEMA_VERSION, records: [] };
43
+ }
44
+ export function readReviewMonitorFile(path) {
45
+ if (!existsSync(path)) {
46
+ return { data: emptyReviewMonitorFile(), error: null };
47
+ }
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(readFileSync(path, "utf8"));
51
+ }
52
+ catch (exc) {
53
+ return { data: null, error: `${path}: invalid JSON (${String(exc)}).` };
54
+ }
55
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
56
+ return { data: null, error: `${path}: review-monitor file must be a JSON object.` };
57
+ }
58
+ const obj = parsed;
59
+ const recordsRaw = obj.records;
60
+ const records = [];
61
+ if (Array.isArray(recordsRaw)) {
62
+ for (const entry of recordsRaw) {
63
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
64
+ continue;
65
+ }
66
+ const e = entry;
67
+ const pr = e.pr;
68
+ const monitorAgentId = e.monitor_agent_id;
69
+ const platformPrimitive = e.platform_primitive;
70
+ const startedAt = e.started_at;
71
+ const worktreePath = e.worktree_path;
72
+ if (typeof pr !== "number" ||
73
+ !Number.isInteger(pr) ||
74
+ pr <= 0 ||
75
+ typeof monitorAgentId !== "string" ||
76
+ monitorAgentId.trim().length === 0 ||
77
+ typeof platformPrimitive !== "string" ||
78
+ typeof startedAt !== "string" ||
79
+ typeof worktreePath !== "string") {
80
+ continue;
81
+ }
82
+ records.push({
83
+ pr,
84
+ repo: typeof e.repo === "string" ? e.repo : null,
85
+ head_sha: typeof e.head_sha === "string" ? e.head_sha : null,
86
+ platform_primitive: platformPrimitive,
87
+ monitor_agent_id: monitorAgentId.trim(),
88
+ started_at: startedAt,
89
+ worktree_path: worktreePath,
90
+ parent_session_id: typeof e.parent_session_id === "string" ? e.parent_session_id : null,
91
+ ended_at: typeof e.ended_at === "string" ? e.ended_at : null,
92
+ });
93
+ }
94
+ }
95
+ return {
96
+ data: {
97
+ schema_version: typeof obj.schema_version === "number" ? obj.schema_version : SCHEMA_VERSION,
98
+ records,
99
+ },
100
+ error: null,
101
+ };
102
+ }
103
+ export function writeReviewMonitorFile(path, data) {
104
+ atomicWriteJson(path, data);
105
+ }
106
+ export function registerReviewMonitor(input) {
107
+ const path = reviewMonitorPath(input.projectRoot);
108
+ const { data, error } = readReviewMonitorFile(path);
109
+ if (data === null) {
110
+ throw new Error(error ?? "could not read review-monitor file");
111
+ }
112
+ const startedAt = (input.startedAt ?? new Date()).toISOString();
113
+ const record = {
114
+ pr: input.pr,
115
+ repo: input.repo ?? null,
116
+ head_sha: input.headSha ?? null,
117
+ platform_primitive: input.platformPrimitive,
118
+ monitor_agent_id: input.monitorAgentId.trim(),
119
+ started_at: startedAt,
120
+ worktree_path: resolve(input.projectRoot),
121
+ parent_session_id: input.parentSessionId ?? null,
122
+ ended_at: null,
123
+ };
124
+ const active = data.records.filter((r) => r.ended_at === null && r.pr !== input.pr);
125
+ writeReviewMonitorFile(path, {
126
+ schema_version: SCHEMA_VERSION,
127
+ records: [...active, record],
128
+ });
129
+ return { path, record };
130
+ }
131
+ export function parseIso8601Utc(value) {
132
+ if (typeof value !== "string" || value.length === 0) {
133
+ return null;
134
+ }
135
+ const trimmed = value.trim();
136
+ let candidate = trimmed;
137
+ if (trimmed.endsWith("Z")) {
138
+ candidate = `${trimmed.slice(0, -1)}+00:00`;
139
+ }
140
+ if (!candidate.endsWith("+00:00")) {
141
+ return null;
142
+ }
143
+ const parsed = new Date(candidate);
144
+ if (Number.isNaN(parsed.getTime())) {
145
+ return null;
146
+ }
147
+ return parsed;
148
+ }
149
+ export function isRecordActive(record, options) {
150
+ if (record.ended_at !== null) {
151
+ return false;
152
+ }
153
+ const started = parseIso8601Utc(record.started_at);
154
+ if (started === null) {
155
+ return false;
156
+ }
157
+ const now = options.now ?? new Date();
158
+ const staleMinutes = options.staleMinutes ?? DEFAULT_STALE_MINUTES;
159
+ const ageMs = now.getTime() - started.getTime();
160
+ if (ageMs > staleMinutes * 60 * 1000) {
161
+ return false;
162
+ }
163
+ if (options.headSha !== undefined && options.headSha !== null && record.head_sha !== null) {
164
+ return record.head_sha === options.headSha;
165
+ }
166
+ return true;
167
+ }
168
+ export function findActiveMonitorForPr(file, pr, options) {
169
+ const matches = file.records.filter((r) => r.pr === pr && isRecordActive(r, options));
170
+ if (matches.length === 0) {
171
+ return null;
172
+ }
173
+ return matches.sort((a, b) => b.started_at.localeCompare(a.started_at))[0] ?? null;
174
+ }
175
+ export function defaultSubagentStatusDir(projectRoot) {
176
+ return join(resolve(projectRoot), ".deft-scratch", "subagent-status");
177
+ }
178
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1,15 @@
1
+ import { MONITORING_TIER_1, MONITORING_TIER_2, MONITORING_TIER_3 } from "./constants.js";
2
+ export type PlatformPrimitive = "start_agent" | "spawn_subagent" | "cursor-task";
3
+ export interface MonitoringTierProbe {
4
+ readonly tier: typeof MONITORING_TIER_1 | typeof MONITORING_TIER_2 | typeof MONITORING_TIER_3;
5
+ readonly primitive: PlatformPrimitive | null;
6
+ readonly descriptor: string | null;
7
+ }
8
+ /**
9
+ * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
10
+ * (#1877 / #2655). Prefer `task platform:capabilities` when available (#1357);
11
+ * this probe does not block MVP.
12
+ */
13
+ export declare function probeMonitoringTier(environ?: NodeJS.ProcessEnv): MonitoringTierProbe;
14
+ export declare function isTier1(probe: MonitoringTierProbe): boolean;
15
+ //# sourceMappingURL=tier-detection.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { MONITORING_TIER_1, MONITORING_TIER_2, MONITORING_TIER_3 } from "./constants.js";
2
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
3
+ function envTruthy(environ, name) {
4
+ return TRUTHY.has((environ[name] ?? "").trim().toLowerCase());
5
+ }
6
+ function probeOverride(environ) {
7
+ const raw = (environ.DEFT_MONITOR_TIER ?? environ.DEFT_MONITOR_TIER_OVERRIDE ?? "").trim();
8
+ if (raw === "1" || raw.toLowerCase() === "tier1") {
9
+ const primitive = environ.DEFT_MONITOR_TIER1_PRIMITIVE ?? "cursor-task";
10
+ return { tier: MONITORING_TIER_1, primitive, descriptor: "override-tier1" };
11
+ }
12
+ if (raw === "3" || raw.toLowerCase() === "tier3") {
13
+ return { tier: MONITORING_TIER_3, primitive: null, descriptor: "generic-terminal" };
14
+ }
15
+ return null;
16
+ }
17
+ /**
18
+ * Inline Tier-1 detection aligned with the swarm Phase 3 / review-cycle matrix
19
+ * (#1877 / #2655). Prefer `task platform:capabilities` when available (#1357);
20
+ * this probe does not block MVP.
21
+ */
22
+ export function probeMonitoringTier(environ = process.env) {
23
+ const override = probeOverride(environ);
24
+ if (override !== null) {
25
+ return override;
26
+ }
27
+ if (envTruthy(environ, "DEFT_PROBE_START_AGENT") || envTruthy(environ, "DEFT_HAS_START_AGENT")) {
28
+ return { tier: MONITORING_TIER_1, primitive: "start_agent", descriptor: "warp-orchestrated" };
29
+ }
30
+ if (envTruthy(environ, "WARP_IS_WARP_TERMINAL") || envTruthy(environ, "WARP_TERMINAL_SESSION")) {
31
+ return { tier: MONITORING_TIER_1, primitive: "start_agent", descriptor: "warp-manual" };
32
+ }
33
+ if (envTruthy(environ, "CURSOR_COMPOSER")) {
34
+ return { tier: MONITORING_TIER_1, primitive: "cursor-task", descriptor: "cursor-composer" };
35
+ }
36
+ if (envTruthy(environ, "CURSOR_AGENT")) {
37
+ return {
38
+ tier: MONITORING_TIER_1,
39
+ primitive: "cursor-task",
40
+ descriptor: "cursor-cloud-agent",
41
+ };
42
+ }
43
+ const runtime = (environ.DEFT_AGENT_RUNTIME ?? "").trim().toLowerCase();
44
+ if (envTruthy(environ, "DEFT_PROBE_GROK_BUILD") ||
45
+ envTruthy(environ, "GROK_BUILD") ||
46
+ runtime === "grok-build") {
47
+ return { tier: MONITORING_TIER_1, primitive: "spawn_subagent", descriptor: "grok-build" };
48
+ }
49
+ if (envTruthy(environ, "DEFT_PROBE_SPAWN_SUBAGENT") ||
50
+ envTruthy(environ, "DEFT_HAS_SPAWN_SUBAGENT")) {
51
+ return { tier: MONITORING_TIER_1, primitive: "spawn_subagent", descriptor: "grok-build" };
52
+ }
53
+ if (envTruthy(environ, "DEFT_MONITOR_TIER2") || envTruthy(environ, "DEFT_HAS_AUTO_REINVOKE")) {
54
+ return { tier: MONITORING_TIER_2, primitive: null, descriptor: "yield-between-polls" };
55
+ }
56
+ return { tier: MONITORING_TIER_3, primitive: null, descriptor: "generic-terminal" };
57
+ }
58
+ export function isTier1(probe) {
59
+ return probe.tier === MONITORING_TIER_1;
60
+ }
61
+ //# sourceMappingURL=tier-detection.js.map
@@ -0,0 +1,32 @@
1
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3 } from "./constants.js";
2
+ import { type ReviewMonitorRecord } from "./record.js";
3
+ import { type MonitoringTierProbe } from "./tier-detection.js";
4
+ export type ReviewMonitorCallSite = "solo" | "swarm-phase5-6" | "swarm-phase6-cascade" | "unspecified";
5
+ export interface VerifyReviewMonitorArgs {
6
+ readonly pr: number;
7
+ readonly projectRoot: string;
8
+ readonly repo?: string | null;
9
+ readonly headSha?: string | null;
10
+ readonly callSite?: ReviewMonitorCallSite;
11
+ readonly approach3?: boolean;
12
+ readonly approach3Warned?: boolean;
13
+ readonly staleMinutes?: number;
14
+ readonly now?: Date;
15
+ readonly environ?: NodeJS.ProcessEnv;
16
+ }
17
+ export interface VerifyReviewMonitorResult {
18
+ readonly exitCode: typeof EXIT_READY | typeof EXIT_NOT_READY | typeof EXIT_CONFIG_ERROR;
19
+ readonly message: string;
20
+ readonly tier: MonitoringTierProbe;
21
+ readonly monitorRecord: ReviewMonitorRecord | null;
22
+ readonly heartbeatActive: boolean;
23
+ readonly callSite: ReviewMonitorCallSite;
24
+ }
25
+ export declare function hasActivePollingHeartbeat(projectRoot: string, pr: number, options?: {
26
+ now?: Date;
27
+ staleMinutes?: number;
28
+ }): boolean;
29
+ export declare function evaluateReviewMonitorGate(args: VerifyReviewMonitorArgs): VerifyReviewMonitorResult;
30
+ export declare function verifyResultToJson(result: VerifyReviewMonitorResult): Record<string, unknown>;
31
+ export { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3 };
32
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1,171 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { sweepScratchDirs } from "../orchestration/subagent-monitor.js";
4
+ import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3, } from "./constants.js";
5
+ import { defaultSubagentStatusDir, findActiveMonitorForPr, readReviewMonitorFile, reviewMonitorPath, } from "./record.js";
6
+ import { isTier1, probeMonitoringTier } from "./tier-detection.js";
7
+ function spawnRedirect(probe) {
8
+ const primitive = probe.primitive ?? "sub-agent";
9
+ return (`Spawn an Approach 1 review-monitor via ${primitive} (background), include ` +
10
+ "`templates/agent-prompt-preamble.md` and `templates/swarm-greptile-poller-prompt.md`, " +
11
+ "then register:\n" +
12
+ " task review-monitor:register -- --pr <N> --monitor-agent-id <id> " +
13
+ `--platform-primitive ${primitive}\n` +
14
+ "Re-run: task verify:review-monitor -- --pr <N>");
15
+ }
16
+ export function hasActivePollingHeartbeat(projectRoot, pr, options = {}) {
17
+ const dir = defaultSubagentStatusDir(projectRoot);
18
+ if (!existsSync(dir)) {
19
+ return false;
20
+ }
21
+ try {
22
+ if (!statSync(dir).isDirectory()) {
23
+ return false;
24
+ }
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ const result = sweepScratchDirs([{ readPath: dir, label: dir }], {
30
+ thresholdMinutes: options.staleMinutes ?? 30,
31
+ now: options.now,
32
+ });
33
+ return result.records.some((rec) => rec.pr_number === pr &&
34
+ rec.failures.length === 0 &&
35
+ !rec.is_stale &&
36
+ !rec.is_terminal &&
37
+ (rec.phase === "polling" || rec.phase === "starting"));
38
+ }
39
+ export function evaluateReviewMonitorGate(args) {
40
+ const projectRoot = resolve(args.projectRoot);
41
+ let isDir = false;
42
+ try {
43
+ isDir = statSync(projectRoot).isDirectory();
44
+ }
45
+ catch {
46
+ isDir = false;
47
+ }
48
+ if (!isDir) {
49
+ return {
50
+ exitCode: EXIT_CONFIG_ERROR,
51
+ message: `verify_review_monitor: --project-root is not a directory: ${projectRoot}`,
52
+ tier: probeMonitoringTier(args.environ),
53
+ monitorRecord: null,
54
+ heartbeatActive: false,
55
+ callSite: args.callSite ?? "unspecified",
56
+ };
57
+ }
58
+ const tier = probeMonitoringTier(args.environ);
59
+ const callSite = args.callSite ?? "unspecified";
60
+ const staleMinutes = args.staleMinutes ?? 30;
61
+ const now = args.now ?? new Date();
62
+ if (args.approach3 === true) {
63
+ if (isTier1(tier)) {
64
+ return {
65
+ exitCode: EXIT_NOT_READY,
66
+ message: "verify_review_monitor: Approach 3 blocking poll is forbidden when Tier 1 is available (#2655).\n" +
67
+ ` Detected tier=${tier.tier} descriptor=${tier.descriptor ?? "unknown"} primitive=${tier.primitive ?? "none"}.\n` +
68
+ ` ${spawnRedirect(tier)}`,
69
+ tier,
70
+ monitorRecord: null,
71
+ heartbeatActive: false,
72
+ callSite,
73
+ };
74
+ }
75
+ if (tier.tier === MONITORING_TIER_3 && args.approach3Warned !== true) {
76
+ return {
77
+ exitCode: EXIT_NOT_READY,
78
+ message: "verify_review_monitor: Approach 3 requires explicit user warning acknowledgment (#2655).\n" +
79
+ " Warn the operator that the conversation pane will lock during polling, then re-run with --approach3-warned.",
80
+ tier,
81
+ monitorRecord: null,
82
+ heartbeatActive: false,
83
+ callSite,
84
+ };
85
+ }
86
+ return {
87
+ exitCode: EXIT_READY,
88
+ message: `verify_review_monitor: Tier 3 Approach 3 path allowed (call-site=${callSite}).`,
89
+ tier,
90
+ monitorRecord: null,
91
+ heartbeatActive: false,
92
+ callSite,
93
+ };
94
+ }
95
+ if (!isTier1(tier)) {
96
+ return {
97
+ exitCode: EXIT_READY,
98
+ message: `verify_review_monitor: Tier ${tier.tier} (${tier.descriptor ?? "unknown"}) — ` +
99
+ "no active review-monitor required (#2655).",
100
+ tier,
101
+ monitorRecord: null,
102
+ heartbeatActive: false,
103
+ callSite,
104
+ };
105
+ }
106
+ const path = reviewMonitorPath(projectRoot);
107
+ const { data, error } = readReviewMonitorFile(path);
108
+ if (data === null) {
109
+ return {
110
+ exitCode: EXIT_CONFIG_ERROR,
111
+ message: `verify_review_monitor: ${error ?? "could not read review-monitor state"}`,
112
+ tier,
113
+ monitorRecord: null,
114
+ heartbeatActive: false,
115
+ callSite,
116
+ };
117
+ }
118
+ const monitorRecord = findActiveMonitorForPr(data, args.pr, {
119
+ now,
120
+ staleMinutes,
121
+ headSha: args.headSha ?? null,
122
+ });
123
+ const heartbeatActive = hasActivePollingHeartbeat(projectRoot, args.pr, {
124
+ now,
125
+ staleMinutes,
126
+ });
127
+ if (monitorRecord !== null || heartbeatActive) {
128
+ const via = monitorRecord !== null ? "review-monitor record" : "subagent heartbeat";
129
+ return {
130
+ exitCode: EXIT_READY,
131
+ message: `verify_review_monitor: active review-monitor for PR #${args.pr} via ${via} ` +
132
+ `(call-site=${callSite}, tier=1, descriptor=${tier.descriptor ?? "unknown"}).`,
133
+ tier,
134
+ monitorRecord,
135
+ heartbeatActive,
136
+ callSite,
137
+ };
138
+ }
139
+ const siteHint = callSite === "swarm-phase5-6"
140
+ ? "Swarm Phase 5→6 handoff (#1386)"
141
+ : callSite === "swarm-phase6-cascade"
142
+ ? "Swarm Phase 6 post force-push (#380)"
143
+ : "Solo drive-to merge-ready / review-cycle ownership";
144
+ return {
145
+ exitCode: EXIT_NOT_READY,
146
+ message: `verify_review_monitor: Tier 1 available but no active review-monitor for PR #${args.pr} (#2655).\n` +
147
+ ` Call site: ${siteHint}.\n` +
148
+ ` Detected descriptor=${tier.descriptor ?? "unknown"} primitive=${tier.primitive ?? "none"}.\n` +
149
+ ` ${spawnRedirect(tier)}`,
150
+ tier,
151
+ monitorRecord: null,
152
+ heartbeatActive: false,
153
+ callSite,
154
+ };
155
+ }
156
+ export function verifyResultToJson(result) {
157
+ return {
158
+ call_site: result.callSite,
159
+ exit_code: result.exitCode,
160
+ heartbeat_active: result.heartbeatActive,
161
+ message: result.message,
162
+ monitor_agent_id: result.monitorRecord?.monitor_agent_id ?? null,
163
+ monitor_record: result.monitorRecord,
164
+ ready: result.exitCode === EXIT_READY,
165
+ tier: result.tier.tier,
166
+ tier_descriptor: result.tier.descriptor,
167
+ tier_primitive: result.tier.primitive,
168
+ };
169
+ }
170
+ export { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3 };
171
+ //# sourceMappingURL=verify.js.map
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
4
5
  import { stripTrailingPathSeparators } from "../text/redos-safe.js";
5
6
  import { append, canonicalLogPath, latestForPath, newDecisionId } from "./audit-log.js";
@@ -51,6 +52,15 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
51
52
  auditEntry: null,
52
53
  };
53
54
  }
55
+ try {
56
+ assertWriteTargetSafe(projectRoot, resolved);
57
+ }
58
+ catch (err) {
59
+ if (err instanceof ProjectionContainmentError) {
60
+ return { ok: false, message: err.message, auditEntry: null };
61
+ }
62
+ throw err;
63
+ }
54
64
  let data;
55
65
  try {
56
66
  data = JSON.parse(readFileSync(resolved, "utf8"));
@@ -129,6 +139,16 @@ export function batchDemote(projectRoot, olderThanDays, options = {}) {
129
139
  .sort();
130
140
  for (const name of files) {
131
141
  const candidate = join(pendingDir, name);
142
+ try {
143
+ assertWriteTargetSafe(projectRoot, candidate);
144
+ }
145
+ catch (err) {
146
+ if (err instanceof ProjectionContainmentError) {
147
+ skipped.push(`${name}: ${err.message}`);
148
+ continue;
149
+ }
150
+ throw err;
151
+ }
132
152
  let data;
133
153
  try {
134
154
  data = JSON.parse(readFileSync(candidate, "utf8"));
@@ -18,6 +18,9 @@ export declare class DuplicateStoryError extends WorktreeMapError {
18
18
  export declare class WorktreeMapConfigError extends Error {
19
19
  name: string;
20
20
  }
21
+ export declare class WorktreePathEscapeError extends WorktreeMapError {
22
+ name: string;
23
+ }
21
24
  export type GitRunner = (args: readonly string[], cwd: string) => TextCaptureResult;
22
25
  export declare const defaultGitRunner: GitRunner;
23
26
  /** Case-normalized comparison key for worktree-path equality. */
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, readFileSync } from "node:fs";
2
- import { resolve as pathResolve } from "node:path";
2
+ import { isAbsolute, resolve as pathResolve } from "node:path";
3
+ import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
3
4
  import { C3_FIELDS } from "./constants.js";
4
5
  import { runText } from "./subprocess.js";
5
6
  export class WorktreeMapError extends Error {
@@ -20,11 +21,25 @@ export class DuplicateStoryError extends WorktreeMapError {
20
21
  export class WorktreeMapConfigError extends Error {
21
22
  name = "WorktreeMapConfigError";
22
23
  }
24
+ export class WorktreePathEscapeError extends WorktreeMapError {
25
+ name = "WorktreePathEscapeError";
26
+ }
23
27
  export const defaultGitRunner = (args, cwd) => runText(["git", ...args], { cwd });
24
28
  function resolvePath(raw, repoRoot) {
25
- const candidate = raw.startsWith("/") ? raw : pathResolve(repoRoot, raw);
29
+ const candidate = isAbsolute(raw) ? raw : pathResolve(repoRoot, raw);
26
30
  return pathResolve(candidate);
27
31
  }
32
+ function assertWorktreePathContained(repoRoot, worktreePath) {
33
+ try {
34
+ assertProjectionContained(repoRoot, worktreePath);
35
+ }
36
+ catch (err) {
37
+ if (err instanceof ProjectionContainmentError) {
38
+ throw new WorktreePathEscapeError(`worktree_path must stay under the repository root: ${err.message}`);
39
+ }
40
+ throw err;
41
+ }
42
+ }
28
43
  /** Case-normalized comparison key for worktree-path equality. */
29
44
  export function compareKey(pathStr) {
30
45
  return pathStr.replace(/\\/g, "/").toLowerCase();
@@ -119,6 +134,7 @@ export function resolveWorktreeMap(mapping, baseBranch, createMissing = true, op
119
134
  }
120
135
  }
121
136
  const worktreePath = resolvePath(rawPath.trim(), root);
137
+ assertWorktreePathContained(root, worktreePath);
122
138
  const key = compareKey(worktreePath);
123
139
  const posixPath = worktreePath.replace(/\\/g, "/");
124
140
  if (seenPaths.has(key)) {
@@ -1,13 +1,14 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
+ import { LEGACY_VBRIEF_VERSION } from "@deftai/directive-types";
4
5
  const UNRELEASED_RE = /## \[Unreleased\][ \t]*\n([\s\S]*?)(?=\n## \[|$)/;
5
6
  const CHANGE_NAME_RE = /^[\w][\w-]*$/;
6
7
  const COMMIT_TYPES = "feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert";
7
8
  const COMMIT_SUBJECT_RE = new RegExp(`^(${COMMIT_TYPES})(\\(.+\\))?!?: .+`);
8
9
  function proposalTemplate(name) {
9
10
  return {
10
- vBRIEFInfo: { version: "0.5" },
11
+ vBRIEFInfo: { version: LEGACY_VBRIEF_VERSION },
11
12
  plan: {
12
13
  title: name,
13
14
  status: "draft",
@@ -26,7 +27,7 @@ function proposalTemplate(name) {
26
27
  }
27
28
  function tasksTemplate(name) {
28
29
  return {
29
- vBRIEFInfo: { version: "0.5" },
30
+ vBRIEFInfo: { version: LEGACY_VBRIEF_VERSION },
30
31
  plan: { title: name, status: "draft", items: [], edges: [] },
31
32
  };
32
33
  }
@@ -1,12 +1,20 @@
1
+ /** Matches Vitest v8 chunk paths such as coverage/.tmp/coverage-0.json (#2580 / #2634). */
2
+ export declare const COVERAGE_TMP_CHUNK_RE: RegExp;
3
+ export declare function isCoverageTmpChunkPath(filePath: string): boolean;
4
+ export declare function ensureCoverageTmpDir(coverageTmpDir?: string): void;
1
5
  /**
2
- * Win32 globalSetup for coverage runs (#2580).
6
+ * Vitest 3.2.x writes coverage chunks without mkdir'ing the parent directory
7
+ * (fixed upstream in vitest 4.x — vitest-dev/vitest#10117). On Windows the
8
+ * directory can disappear between clean() and writeFile under release-scale
9
+ * load; mkdir immediately before chunk writes closes that race without
10
+ * soft-failing real threshold failures (#2634).
11
+ */
12
+ export declare function installCoverageTmpWriteGuard(): () => void;
13
+ /**
14
+ * Win32 globalSetup for coverage runs (#2580, hardened #2634).
3
15
  *
4
- * Vitest's v8 provider writes per-suite JSON under coverage/.tmp without always
5
- * re-mkdir'ing before writeFile. Under parallel fork load the directory can
6
- * disappear mid-suite, surfacing as ENOENT after an otherwise green run.
7
- * Keep the directory present for the coordinator process; late ENOENT flakes
8
- * are tolerated via vitest dangerouslyIgnoreUnhandledErrors (#2546) without
9
- * soft-failing real coverage threshold failures.
16
+ * Keepalive mkdir plus a writeFile guard mirror vitest 4's defensive mkdir until
17
+ * directive upgrades past vitest@3 (see vitest.config.ts #2634 upgrade note).
10
18
  */
11
- export default function setup(): void;
19
+ export default function setup(): () => void;
12
20
  //# sourceMappingURL=win32-coverage-tmp-setup.d.ts.map
@@ -1,24 +1,51 @@
1
- import { mkdirSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- const COVERAGE_TMP = resolve(process.cwd(), "coverage", ".tmp");
4
- function ensureCoverageTmpDir() {
5
- mkdirSync(COVERAGE_TMP, { recursive: true });
1
+ import { promises as fsPromises, mkdirSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ /** Matches Vitest v8 chunk paths such as coverage/.tmp/coverage-0.json (#2580 / #2634). */
4
+ export const COVERAGE_TMP_CHUNK_RE = /[/\\]coverage[/\\]\.tmp[/\\]coverage-\d+\.json$/;
5
+ const defaultCoverageTmp = resolve(process.cwd(), "coverage", ".tmp");
6
+ export function isCoverageTmpChunkPath(filePath) {
7
+ return COVERAGE_TMP_CHUNK_RE.test(filePath.replace(/\\/g, "/"));
8
+ }
9
+ export function ensureCoverageTmpDir(coverageTmpDir = defaultCoverageTmp) {
10
+ mkdirSync(coverageTmpDir, { recursive: true });
11
+ }
12
+ /**
13
+ * Vitest 3.2.x writes coverage chunks without mkdir'ing the parent directory
14
+ * (fixed upstream in vitest 4.x — vitest-dev/vitest#10117). On Windows the
15
+ * directory can disappear between clean() and writeFile under release-scale
16
+ * load; mkdir immediately before chunk writes closes that race without
17
+ * soft-failing real threshold failures (#2634).
18
+ */
19
+ export function installCoverageTmpWriteGuard() {
20
+ const originalWriteFile = fsPromises.writeFile.bind(fsPromises);
21
+ const patchedWriteFile = async (path, ...args) => {
22
+ const target = typeof path === "string" ? path : String(path);
23
+ if (isCoverageTmpChunkPath(target)) {
24
+ mkdirSync(dirname(target), { recursive: true });
25
+ }
26
+ return originalWriteFile(path, ...args);
27
+ };
28
+ fsPromises.writeFile = patchedWriteFile;
29
+ return () => {
30
+ fsPromises.writeFile = originalWriteFile;
31
+ };
6
32
  }
7
33
  /**
8
- * Win32 globalSetup for coverage runs (#2580).
34
+ * Win32 globalSetup for coverage runs (#2580, hardened #2634).
9
35
  *
10
- * Vitest's v8 provider writes per-suite JSON under coverage/.tmp without always
11
- * re-mkdir'ing before writeFile. Under parallel fork load the directory can
12
- * disappear mid-suite, surfacing as ENOENT after an otherwise green run.
13
- * Keep the directory present for the coordinator process; late ENOENT flakes
14
- * are tolerated via vitest dangerouslyIgnoreUnhandledErrors (#2546) without
15
- * soft-failing real coverage threshold failures.
36
+ * Keepalive mkdir plus a writeFile guard mirror vitest 4's defensive mkdir until
37
+ * directive upgrades past vitest@3 (see vitest.config.ts #2634 upgrade note).
16
38
  */
17
39
  export default function setup() {
18
40
  if (process.platform !== "win32")
19
- return;
41
+ return () => { };
20
42
  ensureCoverageTmpDir();
21
- const keepalive = setInterval(ensureCoverageTmpDir, 100);
43
+ const uninstallWriteGuard = installCoverageTmpWriteGuard();
44
+ const keepalive = setInterval(() => ensureCoverageTmpDir(), 50);
22
45
  keepalive.unref?.();
46
+ return () => {
47
+ clearInterval(keepalive);
48
+ uninstallWriteGuard();
49
+ };
23
50
  }
24
51
  //# sourceMappingURL=win32-coverage-tmp-setup.js.map