@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.
Files changed (101) hide show
  1. package/dist/cache/empty-populate.d.ts +44 -0
  2. package/dist/cache/empty-populate.js +117 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/check-updates/index.d.ts +10 -0
  6. package/dist/check-updates/index.js +81 -1
  7. package/dist/doctor/main.js +13 -2
  8. package/dist/doctor/payload-staleness.js +7 -1
  9. package/dist/hooks/dispatcher.d.ts +14 -2
  10. package/dist/hooks/dispatcher.js +77 -20
  11. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  12. package/dist/init-deposit/agent-hooks.js +10 -0
  13. package/dist/init-deposit/hygiene.js +1 -0
  14. package/dist/init-deposit/init-deposit.js +2 -0
  15. package/dist/init-deposit/prettierignore.d.ts +21 -0
  16. package/dist/init-deposit/prettierignore.js +83 -0
  17. package/dist/init-deposit/refresh.d.ts +3 -1
  18. package/dist/init-deposit/refresh.js +29 -40
  19. package/dist/init-deposit/scaffold.js +6 -12
  20. package/dist/init-deposit/xbrief-projections.d.ts +18 -0
  21. package/dist/init-deposit/xbrief-projections.js +117 -0
  22. package/dist/intake/reconcile-issues.d.ts +10 -0
  23. package/dist/intake/reconcile-issues.js +121 -3
  24. package/dist/issue-sync/index.d.ts +2 -0
  25. package/dist/issue-sync/index.js +2 -0
  26. package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
  27. package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
  28. package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
  29. package/dist/issue-sync/sync-from-xbrief.js +208 -0
  30. package/dist/platform/index.d.ts +1 -0
  31. package/dist/platform/index.js +1 -0
  32. package/dist/platform/platform-capabilities.d.ts +5 -0
  33. package/dist/platform/platform-capabilities.js +9 -0
  34. package/dist/platform/shell-context.d.ts +30 -0
  35. package/dist/platform/shell-context.js +111 -0
  36. package/dist/pr-monitor/cadence.d.ts +6 -0
  37. package/dist/pr-monitor/cadence.js +13 -0
  38. package/dist/pr-monitor/index.d.ts +1 -1
  39. package/dist/pr-monitor/index.js +1 -1
  40. package/dist/pr-monitor/monitor.d.ts +7 -0
  41. package/dist/pr-monitor/monitor.js +59 -13
  42. package/dist/pr-wait-mergeable/wrappers.d.ts +9 -0
  43. package/dist/pr-wait-mergeable/wrappers.js +31 -2
  44. package/dist/preflight-cache/evaluate.d.ts +3 -8
  45. package/dist/preflight-cache/evaluate.js +19 -19
  46. package/dist/release/constants.d.ts +2 -0
  47. package/dist/release/constants.js +10 -1
  48. package/dist/release/flags.js +26 -0
  49. package/dist/release/issue-state-fetch.d.ts +27 -0
  50. package/dist/release/issue-state-fetch.js +146 -0
  51. package/dist/release/main.js +1 -0
  52. package/dist/release/native-steps.js +6 -4
  53. package/dist/release/pipeline.js +15 -5
  54. package/dist/release/preflight.d.ts +6 -2
  55. package/dist/release/preflight.js +19 -5
  56. package/dist/release/types.d.ts +3 -1
  57. package/dist/release-e2e/constants.js +6 -6
  58. package/dist/release-e2e/flags.d.ts +2 -0
  59. package/dist/release-e2e/flags.js +8 -4
  60. package/dist/release-e2e/main.js +15 -9
  61. package/dist/release-e2e/types.d.ts +4 -2
  62. package/dist/resolution/classify.d.ts +1 -1
  63. package/dist/resolution/classify.js +36 -11
  64. package/dist/scope/project-definition-sync.js +2 -150
  65. package/dist/scope/registry-artifact-sync.d.ts +3 -0
  66. package/dist/scope/registry-artifact-sync.js +159 -0
  67. package/dist/scope/specification-sync.d.ts +3 -0
  68. package/dist/scope/specification-sync.js +16 -0
  69. package/dist/scope/transition.js +40 -12
  70. package/dist/session/cache-recovery.d.ts +19 -0
  71. package/dist/session/cache-recovery.js +26 -0
  72. package/dist/session/session-start.d.ts +2 -0
  73. package/dist/session/session-start.js +14 -3
  74. package/dist/session/verify-session-ritual.d.ts +1 -0
  75. package/dist/session/verify-session-ritual.js +1 -0
  76. package/dist/triage/welcome/default-mode.js +2 -0
  77. package/dist/triage/welcome/onboard.js +2 -0
  78. package/dist/ts-check-lane/run-lane.d.ts +5 -0
  79. package/dist/ts-check-lane/run-lane.js +9 -0
  80. package/dist/vbrief-reconcile/labels.d.ts +3 -0
  81. package/dist/vbrief-reconcile/labels.js +14 -0
  82. package/dist/vbrief-reconcile/main.d.ts +1 -0
  83. package/dist/vbrief-reconcile/main.js +13 -3
  84. package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
  85. package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
  86. package/dist/vbrief-reconcile/repo-guard.js +33 -0
  87. package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
  88. package/dist/vbrief-reconcile/umbrellas.js +17 -1
  89. package/dist/verify-env/agent-hooks.js +4 -3
  90. package/dist/verify-env/verify-hooks-installed.js +5 -5
  91. package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
  92. package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
  93. package/dist/vitest-runner/coverage-debt.d.ts +50 -0
  94. package/dist/vitest-runner/coverage-debt.js +155 -0
  95. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
  96. package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
  97. package/dist/xbrief-migrate/detect.js +46 -1
  98. package/dist/xbrief-migrate/migrate-project.js +19 -4
  99. package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
  100. package/dist/xbrief-migrate/migration-containment.js +37 -0
  101. package/package.json +7 -3
@@ -0,0 +1,46 @@
1
+ import { type RunGhApiFn } from "../intake/github-body.js";
2
+ export declare const SYNC_COMMENT_HEADER = "## xBRIEF sync (deft issue:sync-from-xbrief)";
3
+ export interface OriginIssueTarget {
4
+ readonly repo: string;
5
+ readonly number: number;
6
+ readonly uri: string;
7
+ }
8
+ export interface SyncSnapshot {
9
+ readonly status: string;
10
+ readonly title: string;
11
+ readonly acceptance: string;
12
+ readonly items: readonly {
13
+ title: string;
14
+ status: string;
15
+ }[];
16
+ }
17
+ export interface ResolveOriginOptions {
18
+ readonly fallbackRepo?: string | null;
19
+ }
20
+ export declare function resolveOriginIssue(data: Record<string, unknown>, options?: ResolveOriginOptions): OriginIssueTarget | null;
21
+ export declare function extractSyncSnapshot(data: Record<string, unknown>): SyncSnapshot;
22
+ export declare function fingerprintSyncSnapshot(snapshot: SyncSnapshot): string;
23
+ export declare function readStoredFingerprint(data: Record<string, unknown>): string | null;
24
+ export declare function hasMaterialChanges(data: Record<string, unknown>): boolean;
25
+ export declare function sanitizeMarkdownInline(text: string): string;
26
+ export declare function buildSyncComment(data: Record<string, unknown>, xbriefPath: string): string;
27
+ export interface SyncFromXbriefOptions {
28
+ readonly xbriefPath: string;
29
+ readonly dryRun?: boolean;
30
+ readonly projectRoot?: string;
31
+ readonly repo?: string;
32
+ readonly runFn?: RunGhApiFn;
33
+ readonly writeErr?: (message: string) => void;
34
+ readonly writeOut?: (message: string) => void;
35
+ readonly writeFingerprint?: (absPath: string, data: Record<string, unknown>) => void;
36
+ }
37
+ export declare function stampIssueSyncFingerprint(data: Record<string, unknown>, origin: OriginIssueTarget): Record<string, unknown>;
38
+ export declare function syncFromXbrief(options: SyncFromXbriefOptions): number;
39
+ export interface SyncFromXbriefCliArgs {
40
+ readonly path?: string;
41
+ readonly dryRun?: boolean;
42
+ readonly projectRoot?: string;
43
+ readonly repo?: string;
44
+ }
45
+ export declare function syncFromXbriefMain(args: SyncFromXbriefCliArgs): number;
46
+ //# sourceMappingURL=sync-from-xbrief.d.ts.map
@@ -0,0 +1,208 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { createIssueComment } from "../intake/github-body.js";
5
+ import { provenanceIssueNumber, repoSlugFromUrl } from "../intake/issue-ingest.js";
6
+ import { extractReferencesFromVbrief, GITHUB_ISSUE_REF_TYPES, parseIssueNumber, } from "../intake/reconcile-issues.js";
7
+ import { resolveProjectRoot } from "../scope/project-context.js";
8
+ import { resolveProjectRepo } from "../slice/project-context.js";
9
+ export const SYNC_COMMENT_HEADER = "## xBRIEF sync (deft issue:sync-from-xbrief)";
10
+ export function resolveOriginIssue(data, options = {}) {
11
+ const refs = extractReferencesFromVbrief(data);
12
+ const githubRefs = [];
13
+ for (const ref of refs) {
14
+ if (!GITHUB_ISSUE_REF_TYPES.has(String(ref.type ?? ""))) {
15
+ continue;
16
+ }
17
+ const num = parseIssueNumber(ref);
18
+ if (num !== null) {
19
+ githubRefs.push({ ref, number: num });
20
+ }
21
+ }
22
+ if (githubRefs.length === 0) {
23
+ return null;
24
+ }
25
+ const provenanceNum = provenanceIssueNumber(data);
26
+ let chosen = githubRefs[0];
27
+ if (provenanceNum !== null) {
28
+ const match = githubRefs.find((entry) => entry.number === provenanceNum);
29
+ if (match !== undefined) {
30
+ chosen = match;
31
+ }
32
+ }
33
+ const uri = (typeof chosen.ref.uri === "string" && chosen.ref.uri.length > 0 ? chosen.ref.uri : null) ??
34
+ (typeof chosen.ref.url === "string" && chosen.ref.url.length > 0 ? chosen.ref.url : null) ??
35
+ "";
36
+ let repo = repoSlugFromUrl(uri);
37
+ if (repo === null && options.fallbackRepo !== undefined && options.fallbackRepo !== null) {
38
+ repo = options.fallbackRepo;
39
+ }
40
+ if (repo === null) {
41
+ return null;
42
+ }
43
+ return {
44
+ repo,
45
+ number: chosen.number,
46
+ uri: uri.length > 0 ? uri : `https://github.com/${repo}/issues/${chosen.number}`,
47
+ };
48
+ }
49
+ export function extractSyncSnapshot(data) {
50
+ const plan = (data.plan ?? {});
51
+ const narratives = (plan.narratives ?? {});
52
+ const acceptance = typeof narratives.Acceptance === "string"
53
+ ? narratives.Acceptance
54
+ : typeof narratives.acceptance === "string"
55
+ ? narratives.acceptance
56
+ : "";
57
+ const items = [];
58
+ const walk = (raw) => {
59
+ if (!Array.isArray(raw)) {
60
+ return;
61
+ }
62
+ for (const item of raw) {
63
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
64
+ continue;
65
+ }
66
+ const obj = item;
67
+ items.push({
68
+ title: String(obj.title ?? ""),
69
+ status: String(obj.status ?? "unknown"),
70
+ });
71
+ walk(obj.subItems);
72
+ walk(obj.items);
73
+ }
74
+ };
75
+ walk(plan.items);
76
+ return {
77
+ status: String(plan.status ?? "unknown"),
78
+ title: String(plan.title ?? ""),
79
+ acceptance,
80
+ items,
81
+ };
82
+ }
83
+ export function fingerprintSyncSnapshot(snapshot) {
84
+ return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 16);
85
+ }
86
+ export function readStoredFingerprint(data) {
87
+ const plan = (data.plan ?? {});
88
+ const metadata = plan.metadata;
89
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
90
+ return null;
91
+ }
92
+ const issueSync = metadata.issueSync;
93
+ if (issueSync === null || typeof issueSync !== "object" || Array.isArray(issueSync)) {
94
+ return null;
95
+ }
96
+ const fp = issueSync.fingerprint;
97
+ return typeof fp === "string" && fp.length > 0 ? fp : null;
98
+ }
99
+ export function hasMaterialChanges(data) {
100
+ const current = fingerprintSyncSnapshot(extractSyncSnapshot(data));
101
+ const stored = readStoredFingerprint(data);
102
+ return stored === null || stored !== current;
103
+ }
104
+ export function sanitizeMarkdownInline(text) {
105
+ return text.replace(/\r?\n/g, " ");
106
+ }
107
+ export function buildSyncComment(data, xbriefPath) {
108
+ const snapshot = extractSyncSnapshot(data);
109
+ const safeTitle = sanitizeMarkdownInline(snapshot.title);
110
+ const lines = [
111
+ SYNC_COMMENT_HEADER,
112
+ "",
113
+ `**Scope:** ${safeTitle}`,
114
+ `**Status:** \`${sanitizeMarkdownInline(snapshot.status)}\``,
115
+ `**xBRIEF:** \`${xbriefPath}\``,
116
+ ];
117
+ if (snapshot.items.length > 0) {
118
+ lines.push("", "### Plan items", "");
119
+ for (const item of snapshot.items) {
120
+ lines.push(`- **${sanitizeMarkdownInline(item.status)}** — ${sanitizeMarkdownInline(item.title)}`);
121
+ }
122
+ }
123
+ if (snapshot.acceptance.length > 0) {
124
+ lines.push("", "### Acceptance criteria", "", snapshot.acceptance);
125
+ }
126
+ lines.push("", "_Posted by `task issue:sync-from-xbrief`._");
127
+ return lines.join("\n");
128
+ }
129
+ export function stampIssueSyncFingerprint(data, origin) {
130
+ const plan = (data.plan ?? {});
131
+ const metadata = { ...(plan.metadata ?? {}) };
132
+ metadata.issueSync = {
133
+ fingerprint: fingerprintSyncSnapshot(extractSyncSnapshot(data)),
134
+ syncedAt: new Date().toISOString(),
135
+ issueNumber: origin.number,
136
+ repo: origin.repo,
137
+ };
138
+ plan.metadata = metadata;
139
+ return { ...data, plan };
140
+ }
141
+ export function syncFromXbrief(options) {
142
+ const writeErr = options.writeErr ?? ((message) => process.stderr.write(`${message}\n`));
143
+ const writeOut = options.writeOut ?? ((message) => process.stdout.write(`${message}\n`));
144
+ const absPath = resolve(options.xbriefPath);
145
+ let data;
146
+ try {
147
+ data = JSON.parse(readFileSync(absPath, "utf8"));
148
+ }
149
+ catch (exc) {
150
+ writeErr(`issue:sync-from-xbrief: cannot read ${options.xbriefPath}: ${exc instanceof Error ? exc.message : String(exc)}`);
151
+ return 1;
152
+ }
153
+ const projectRoot = resolveProjectRoot(options.projectRoot) ?? process.cwd();
154
+ const fallbackRepo = resolveProjectRepo(options.repo, projectRoot);
155
+ const origin = resolveOriginIssue(data, { fallbackRepo });
156
+ if (origin === null) {
157
+ writeErr("issue:sync-from-xbrief: no linked GitHub issue origin in xBRIEF references (x-xbrief/github-issue); skip or add origin reference.");
158
+ return 1;
159
+ }
160
+ if (!hasMaterialChanges(data)) {
161
+ writeOut(`issue:sync-from-xbrief: no material AC/status changes since last sync for #${origin.number}; nothing to post.`);
162
+ return 0;
163
+ }
164
+ const relPath = options.xbriefPath.replace(/\\/g, "/");
165
+ const comment = buildSyncComment(data, relPath);
166
+ if (options.dryRun) {
167
+ writeOut(`issue:sync-from-xbrief: dry-run would post comment to ${origin.repo}#${origin.number}:\n`);
168
+ writeOut(comment);
169
+ return 0;
170
+ }
171
+ let commentResult;
172
+ try {
173
+ commentResult = createIssueComment(origin.repo, origin.number, {
174
+ body: comment,
175
+ runFn: options.runFn,
176
+ });
177
+ }
178
+ catch (exc) {
179
+ writeErr(`issue:sync-from-xbrief: failed to post comment: ${exc instanceof Error ? exc.message : String(exc)}`);
180
+ return 1;
181
+ }
182
+ writeOut(`issue:sync-from-xbrief: posted comment to ${origin.repo}#${origin.number} (id: ${commentResult.id}).`);
183
+ const writeFingerprint = options.writeFingerprint ??
184
+ ((targetPath, stamped) => {
185
+ writeFileSync(targetPath, `${JSON.stringify(stamped, null, 2)}\n`, "utf8");
186
+ });
187
+ try {
188
+ writeFingerprint(absPath, stampIssueSyncFingerprint(data, origin));
189
+ }
190
+ catch (exc) {
191
+ writeErr(`issue:sync-from-xbrief: comment posted (id: ${commentResult.id}) but failed to persist sync fingerprint in ${options.xbriefPath}: ${exc instanceof Error ? exc.message : String(exc)}`);
192
+ return 1;
193
+ }
194
+ return 0;
195
+ }
196
+ export function syncFromXbriefMain(args) {
197
+ if (args.path === undefined || args.path.length === 0) {
198
+ process.stderr.write("issue:sync-from-xbrief: xBRIEF path required\n");
199
+ return 2;
200
+ }
201
+ return syncFromXbrief({
202
+ xbriefPath: args.path,
203
+ dryRun: args.dryRun,
204
+ projectRoot: args.projectRoot,
205
+ repo: args.repo,
206
+ });
207
+ }
208
+ //# sourceMappingURL=sync-from-xbrief.js.map
@@ -6,5 +6,6 @@ export * from "./linear-scan.js";
6
6
  export * from "./platform-capabilities.js";
7
7
  export * from "./resolve-changelog-unreleased.js";
8
8
  export * from "./resolve-version.js";
9
+ export * from "./shell-context.js";
9
10
  export * from "./slug-normalize.js";
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -6,5 +6,6 @@ export * from "./linear-scan.js";
6
6
  export * from "./platform-capabilities.js";
7
7
  export * from "./resolve-changelog-unreleased.js";
8
8
  export * from "./resolve-version.js";
9
+ export * from "./shell-context.js";
9
10
  export * from "./slug-normalize.js";
10
11
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,4 @@
1
+ import { type ShellContext } from "./shell-context.js";
1
2
  export interface UidMapEntry {
2
3
  readonly insideId: number;
3
4
  readonly outsideId: number;
@@ -10,6 +11,8 @@ export interface OwnershipFacts {
10
11
  readonly interpretedAsSandboxView: boolean;
11
12
  }
12
13
  export interface RuntimeCapabilityReport {
14
+ readonly hostPlatform: NodeJS.Platform;
15
+ readonly shell: ShellContext;
13
16
  readonly runtimeMode: string;
14
17
  readonly identityKind: string;
15
18
  readonly effectiveUid: number | null;
@@ -33,6 +36,8 @@ export declare function classifyIdentityKind(options: {
33
36
  export declare function classifyRuntimeMode(environ: Readonly<Record<string, string>>, sandboxUidRemap: boolean): string;
34
37
  export interface ProbeRuntimeOptions {
35
38
  readonly environ?: Readonly<Record<string, string>>;
39
+ readonly platform?: NodeJS.Platform;
40
+ readonly userShell?: string | null;
36
41
  readonly uidMapPath?: string;
37
42
  readonly cwd?: string;
38
43
  readonly effectiveUidOverride?: number | null;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { userInfo } from "node:os";
3
3
  import { IDENTITY_LOCAL_USER, IDENTITY_REAL_ROOT, IDENTITY_SANDBOX_REMAPPED_LOCAL_USER, IDENTITY_UNKNOWN, RUNTIME_MODE_CLOUD_HEADLESS, RUNTIME_MODE_CURSOR_NATIVE_SANDBOX, RUNTIME_MODE_LOCAL_UNSANDBOXED, } from "./constants.js";
4
+ import { detectEnvironmentContext, environmentContextToDict, } from "./shell-context.js";
4
5
  const TRUTHY = new Set(["1", "true", "yes", "on"]);
5
6
  const CURSOR_SIGNAL_VARS = [
6
7
  "CURSOR_SANDBOX",
@@ -118,6 +119,11 @@ export function probeRuntimeCapabilities(options = {}) {
118
119
  const env = options.environ === undefined
119
120
  ? { ...process.env }
120
121
  : { ...options.environ };
122
+ const environment = detectEnvironmentContext({
123
+ environ: env,
124
+ platform: options.platform,
125
+ userShell: options.userShell,
126
+ });
121
127
  let effectiveUid;
122
128
  if (options.effectiveUidOverride !== undefined) {
123
129
  effectiveUid = options.effectiveUidOverride;
@@ -147,6 +153,8 @@ export function probeRuntimeCapabilities(options = {}) {
147
153
  const cwdPath = options.cwd ?? process.cwd();
148
154
  const ownership = readOwnership(cwdPath, sandboxUidRemap);
149
155
  return {
156
+ hostPlatform: environment.hostPlatform,
157
+ shell: environment.shell,
150
158
  runtimeMode,
151
159
  identityKind,
152
160
  effectiveUid,
@@ -164,6 +172,7 @@ export function getPlatformCapabilities(options = {}) {
164
172
  }
165
173
  export function reportToDict(report) {
166
174
  return {
175
+ ...environmentContextToDict({ hostPlatform: report.hostPlatform, shell: report.shell }),
167
176
  runtime_mode: report.runtimeMode,
168
177
  identity_kind: report.identityKind,
169
178
  effective_uid: report.effectiveUid,
@@ -0,0 +1,30 @@
1
+ /** Describes whether a shell signal names the executor, a user default, or no known shell. */
2
+ export type ShellContextKind = "execution" | "default" | "unknown";
3
+ /** Names the source used to resolve shell orientation. */
4
+ export type ShellContextSource = "DEFT_EXECUTION_SHELL" | "SHELL" | "os.userInfo().shell" | "ComSpec" | "unknown";
5
+ /** Validated shell orientation with explicit semantics and provenance. */
6
+ export interface ShellContext {
7
+ readonly name: string;
8
+ readonly path: string | null;
9
+ readonly kind: ShellContextKind;
10
+ readonly source: ShellContextSource;
11
+ }
12
+ /** Host platform and shell facts safe to surface in session orientation. */
13
+ export interface EnvironmentContext {
14
+ readonly hostPlatform: NodeJS.Platform;
15
+ readonly shell: ShellContext;
16
+ }
17
+ /** Injectable inputs for deterministic shell-context detection. */
18
+ export interface DetectEnvironmentContextOptions {
19
+ readonly environ?: Readonly<Record<string, string | undefined>>;
20
+ readonly platform?: NodeJS.Platform;
21
+ readonly userShell?: string | null;
22
+ readonly readUserShell?: () => string | null;
23
+ }
24
+ /** Detect host platform and the best validated shell signal without inferring executor semantics. */
25
+ export declare function detectEnvironmentContext(options?: DetectEnvironmentContextOptions): EnvironmentContext;
26
+ /** Convert environment orientation to the stable snake-case CLI JSON contract. */
27
+ export declare function environmentContextToDict(context: EnvironmentContext): Record<string, unknown>;
28
+ /** Format one injection-safe, source-attributed session orientation line. */
29
+ export declare function formatEnvironmentContext(context: EnvironmentContext): string;
30
+ //# sourceMappingURL=shell-context.d.ts.map
@@ -0,0 +1,111 @@
1
+ import { userInfo } from "node:os";
2
+ import { posix, win32 } from "node:path";
3
+ const MAX_SHELL_CANDIDATE_LENGTH = 4096;
4
+ function readAccountShell(readUserShell) {
5
+ try {
6
+ return readUserShell();
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ function hasControlCharacter(value) {
13
+ for (const character of value) {
14
+ const codePoint = character.charCodeAt(0);
15
+ if (codePoint <= 31 ||
16
+ (codePoint >= 127 && codePoint <= 159) ||
17
+ codePoint === 0x2028 ||
18
+ codePoint === 0x2029) {
19
+ return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ function normalizeCandidate(candidate) {
25
+ const raw = candidate.value;
26
+ if (raw === null || raw === undefined)
27
+ return null;
28
+ if (raw.length > MAX_SHELL_CANDIDATE_LENGTH || hasControlCharacter(raw))
29
+ return null;
30
+ const value = raw.trim();
31
+ if (!value)
32
+ return null;
33
+ const lastSeparator = Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\"));
34
+ const basename = value.slice(lastSeparator + 1);
35
+ if (!basename)
36
+ return null;
37
+ const name = basename.replace(/\.exe$/i, "");
38
+ if (!name || name === "." || name === "..")
39
+ return null;
40
+ return {
41
+ name,
42
+ path: lastSeparator >= 0 && (posix.isAbsolute(value) || win32.isAbsolute(value)) ? value : null,
43
+ kind: candidate.kind,
44
+ source: candidate.source,
45
+ };
46
+ }
47
+ function renderValue(value) {
48
+ return /^[A-Za-z0-9_./:\\+()-]+$/.test(value) ? value : JSON.stringify(value);
49
+ }
50
+ /** Detect host platform and the best validated shell signal without inferring executor semantics. */
51
+ export function detectEnvironmentContext(options = {}) {
52
+ const environ = options.environ ?? process.env;
53
+ const hostPlatform = options.platform ?? process.platform;
54
+ const accountShell = hostPlatform === "win32"
55
+ ? null
56
+ : options.userShell === undefined
57
+ ? readAccountShell(options.readUserShell ?? (() => userInfo().shell || null))
58
+ : options.userShell;
59
+ const candidates = [
60
+ {
61
+ value: environ.DEFT_EXECUTION_SHELL,
62
+ kind: "execution",
63
+ source: "DEFT_EXECUTION_SHELL",
64
+ },
65
+ { value: environ.SHELL, kind: "default", source: "SHELL" },
66
+ ...(hostPlatform === "win32"
67
+ ? [
68
+ {
69
+ value: environ.ComSpec ?? environ.COMSPEC,
70
+ kind: "default",
71
+ source: "ComSpec",
72
+ },
73
+ ]
74
+ : [
75
+ {
76
+ value: accountShell,
77
+ kind: "default",
78
+ source: "os.userInfo().shell",
79
+ },
80
+ ]),
81
+ ];
82
+ for (const candidate of candidates) {
83
+ const shell = normalizeCandidate(candidate);
84
+ if (shell)
85
+ return { hostPlatform, shell };
86
+ }
87
+ return {
88
+ hostPlatform,
89
+ shell: { name: "unknown", path: null, kind: "unknown", source: "unknown" },
90
+ };
91
+ }
92
+ /** Convert environment orientation to the stable snake-case CLI JSON contract. */
93
+ export function environmentContextToDict(context) {
94
+ return {
95
+ host_platform: context.hostPlatform,
96
+ shell: {
97
+ name: context.shell.name,
98
+ path: context.shell.path,
99
+ kind: context.shell.kind,
100
+ source: context.shell.source,
101
+ },
102
+ };
103
+ }
104
+ /** Format one injection-safe, source-attributed session orientation line. */
105
+ export function formatEnvironmentContext(context) {
106
+ const path = context.shell.path === null ? "unknown" : renderValue(context.shell.path);
107
+ return (`[deft environment] os=${renderValue(context.hostPlatform)}; ` +
108
+ `shell=${renderValue(context.shell.name)}; kind=${context.shell.kind}; ` +
109
+ `path=${path}; source=${context.shell.source}`);
110
+ }
111
+ //# sourceMappingURL=shell-context.js.map
@@ -1,3 +1,9 @@
1
1
  /** Expand cadence tuple into per-poll interval seconds. */
2
2
  export declare function cadenceIntervals(cadence?: ReadonlyArray<readonly [number, number]>): number[];
3
+ /**
4
+ * Seconds to sleep after poll `pollIndex` (1-based). Repeats the final cadence
5
+ * tier once the configured repeats are exhausted so the monitor can run until
6
+ * the cap instead of stopping after a fixed poll budget (#2581).
7
+ */
8
+ export declare function cadenceIntervalAfterPoll(pollIndex: number, cadence?: ReadonlyArray<readonly [number, number]>): number;
3
9
  //# sourceMappingURL=cadence.d.ts.map
@@ -9,4 +9,17 @@ export function cadenceIntervals(cadence = DEFAULT_CADENCE) {
9
9
  }
10
10
  return intervals;
11
11
  }
12
+ /**
13
+ * Seconds to sleep after poll `pollIndex` (1-based). Repeats the final cadence
14
+ * tier once the configured repeats are exhausted so the monitor can run until
15
+ * the cap instead of stopping after a fixed poll budget (#2581).
16
+ */
17
+ export function cadenceIntervalAfterPoll(pollIndex, cadence = DEFAULT_CADENCE) {
18
+ const intervals = cadenceIntervals(cadence);
19
+ if (intervals.length === 0) {
20
+ return 60;
21
+ }
22
+ const idx = Math.min(Math.max(pollIndex, 1) - 1, intervals.length - 1);
23
+ return intervals[idx] ?? intervals[intervals.length - 1] ?? 60;
24
+ }
12
25
  //# sourceMappingURL=cadence.js.map
@@ -1,4 +1,4 @@
1
- export { cadenceIntervals } from "./cadence.js";
1
+ export { cadenceIntervalAfterPoll, cadenceIntervals } from "./cadence.js";
2
2
  export { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
3
3
  export { cmdPrMonitor, parseMonitorArgs, runMonitor } from "./main.js";
4
4
  export { formatPollStatus, isTerminalPrState, mergeStateFromPayload, monitor, summaryLabelForExit, } from "./monitor.js";
@@ -1,4 +1,4 @@
1
- export { cadenceIntervals } from "./cadence.js";
1
+ export { cadenceIntervalAfterPoll, cadenceIntervals } from "./cadence.js";
2
2
  export { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
3
3
  export { cmdPrMonitor, parseMonitorArgs, runMonitor } from "./main.js";
4
4
  export { formatPollStatus, isTerminalPrState, mergeStateFromPayload, monitor, summaryLabelForExit, } from "./monitor.js";
@@ -1,4 +1,11 @@
1
1
  import type { MonitorOptions, MonitorRunResult, PollResult } from "./types.js";
2
+ /**
3
+ * Truncate blocked-on text for one-line heartbeats while preserving
4
+ * distinguishable SHA prefixes during update-branch races (#2581).
5
+ */
6
+ export declare function truncateBlockedOn(failure: string, maxLen?: number): string;
7
+ /** Emit interim stderr heartbeats during long sleeps (#2581 / #2260). */
8
+ export declare function sleepWithCadenceHeartbeats(totalSeconds: number, priorCadenceSeconds: number, nextPollIndex: number, sleepFn: (seconds: number) => void): void;
2
9
  /** Pull GitHub's merge-state signal out of a readiness payload, if present. */
3
10
  export declare function mergeStateFromPayload(payload: Record<string, unknown>): string;
4
11
  /**
@@ -1,4 +1,4 @@
1
- import { cadenceIntervals } from "./cadence.js";
1
+ import { cadenceIntervalAfterPoll } from "./cadence.js";
2
2
  import { DEFAULT_CADENCE, EXIT_CAP_REACHED, EXIT_CLEAN, EXIT_CONFIG_ERROR, EXIT_PR_TERMINAL, } from "./constants.js";
3
3
  import { callReadiness } from "./readiness.js";
4
4
  const systemMonotonicClock = {
@@ -13,6 +13,52 @@ function defaultSleep(seconds) {
13
13
  // busy-wait fallback when no injectable sleep in production CLI path
14
14
  }
15
15
  }
16
+ const GREPTILE_STALE_SHA_RE = /^Greptile last reviewed ([0-9a-f]+) but PR HEAD is ([0-9a-f]+)\./i;
17
+ /**
18
+ * Truncate blocked-on text for one-line heartbeats while preserving
19
+ * distinguishable SHA prefixes during update-branch races (#2581).
20
+ */
21
+ export function truncateBlockedOn(failure, maxLen = 80) {
22
+ const match = GREPTILE_STALE_SHA_RE.exec(failure);
23
+ if (match !== null) {
24
+ const reviewedSha = match[1];
25
+ const headSha = match[2];
26
+ if (reviewedSha !== undefined && headSha !== undefined) {
27
+ const compact = `Greptile last reviewed ${reviewedSha.slice(0, 12)}... ` +
28
+ `but PR HEAD is ${headSha.slice(0, 12)}...`;
29
+ if (compact.length <= maxLen) {
30
+ return compact;
31
+ }
32
+ }
33
+ }
34
+ return failure.slice(0, maxLen);
35
+ }
36
+ /** Emit interim stderr heartbeats during long sleeps (#2581 / #2260). */
37
+ export function sleepWithCadenceHeartbeats(totalSeconds, priorCadenceSeconds, nextPollIndex, sleepFn) {
38
+ if (totalSeconds <= 0) {
39
+ return;
40
+ }
41
+ const maxSilentGap = Math.max(1, priorCadenceSeconds * 2);
42
+ if (totalSeconds <= maxSilentGap) {
43
+ sleepFn(totalSeconds);
44
+ return;
45
+ }
46
+ let remaining = totalSeconds;
47
+ let emittedWait = false;
48
+ while (remaining > 0) {
49
+ const chunk = Math.min(remaining, maxSilentGap);
50
+ if (!emittedWait) {
51
+ process.stderr.write(`[monitor_pr] waiting ${Math.ceil(remaining)}s until poll #${nextPollIndex} ` +
52
+ `(heartbeat cap ${maxSilentGap}s)\n`);
53
+ emittedWait = true;
54
+ }
55
+ else {
56
+ process.stderr.write(`[monitor_pr] still waiting ${Math.ceil(remaining)}s until poll #${nextPollIndex}\n`);
57
+ }
58
+ sleepFn(chunk);
59
+ remaining -= chunk;
60
+ }
61
+ }
16
62
  /** Pull GitHub's merge-state signal out of a readiness payload, if present. */
17
63
  export function mergeStateFromPayload(payload) {
18
64
  const partialRaw = payload.partial_data;
@@ -61,7 +107,7 @@ export function formatPollStatus(pollIndex, pollResult, elapsedSeconds = 0) {
61
107
  let line = `[monitor_pr] poll #${pollIndex} t=${elapsed}s via=${via} head=${headDisplay} ` +
62
108
  `mergeState=${mergeState} ${label} (${failures.length} failures)`;
63
109
  if (firstFailure.length > 0) {
64
- line += ` -- blocked-on: ${firstFailure.slice(0, 80)}`;
110
+ line += ` -- blocked-on: ${truncateBlockedOn(firstFailure)}`;
65
111
  }
66
112
  return line;
67
113
  }
@@ -75,7 +121,7 @@ export function isTerminalPrState(payload) {
75
121
  }
76
122
  /** Loop readiness with adaptive cadence until CLEAN / cap / terminal. */
77
123
  export function monitor(prNumber, repo, options = {}) {
78
- const intervals = cadenceIntervals(options.cadence ?? DEFAULT_CADENCE);
124
+ const cadence = options.cadence ?? DEFAULT_CADENCE;
79
125
  const capSeconds = (options.capMinutes ?? 60) * 60;
80
126
  const clockFn = options.clockFn ?? systemMonotonicClock;
81
127
  const sleepFn = options.sleepFn ?? defaultSleep;
@@ -84,7 +130,8 @@ export function monitor(prNumber, repo, options = {}) {
84
130
  let pollIndex = 0;
85
131
  let lastPayload = {};
86
132
  let lastExit = EXIT_CAP_REACHED;
87
- for (const interval of intervals) {
133
+ let priorCadenceSeconds = cadenceIntervalAfterPoll(1, cadence);
134
+ while (true) {
88
135
  pollIndex += 1;
89
136
  const elapsed = clockFn.now() - startedAt;
90
137
  if (elapsed > capSeconds) {
@@ -106,17 +153,16 @@ export function monitor(prNumber, repo, options = {}) {
106
153
  if (isTerminalPrState(lastPayload)) {
107
154
  return { exitCode: EXIT_PR_TERMINAL, payload: lastPayload, pollCount: pollIndex };
108
155
  }
109
- if (pollIndex < intervals.length) {
110
- const elapsedAfterPoll = clockFn.now() - startedAt;
111
- const remaining = capSeconds - elapsedAfterPoll;
112
- if (remaining <= 0) {
113
- return { exitCode: EXIT_CAP_REACHED, payload: lastPayload, pollCount: pollIndex };
114
- }
115
- sleepFn(Math.min(interval, Math.max(1, Math.trunc(remaining))));
156
+ const elapsedAfterPoll = clockFn.now() - startedAt;
157
+ const remaining = capSeconds - elapsedAfterPoll;
158
+ if (remaining <= 0) {
159
+ const finalExit = lastExit === EXIT_CONFIG_ERROR ? EXIT_CONFIG_ERROR : EXIT_CAP_REACHED;
160
+ return { exitCode: finalExit, payload: lastPayload, pollCount: pollIndex };
116
161
  }
162
+ const sleepSeconds = Math.min(cadenceIntervalAfterPoll(pollIndex, cadence), Math.max(1, Math.trunc(remaining)));
163
+ sleepWithCadenceHeartbeats(sleepSeconds, priorCadenceSeconds, pollIndex + 1, sleepFn);
164
+ priorCadenceSeconds = sleepSeconds;
117
165
  }
118
- const finalExit = lastExit === EXIT_CONFIG_ERROR ? EXIT_CONFIG_ERROR : EXIT_CAP_REACHED;
119
- return { exitCode: finalExit, payload: lastPayload, pollCount: pollIndex };
120
166
  }
121
167
  export const summaryLabelForExit = (exitCode) => {
122
168
  switch (exitCode) {
@@ -17,6 +17,15 @@ export interface CaptureExecOptions {
17
17
  }
18
18
  /** UTF-8-safe subprocess capture via spawnSync (no shell) — mirrors #1366. */
19
19
  export declare function captureExec(executable: string, args: readonly string[], timeoutMs: number, options?: CaptureExecOptions): CaptureExecResult;
20
+ /**
21
+ * Resolve a CLI entry script for subprocess spawn (#2615).
22
+ *
23
+ * Published npm layout nests `@deftai/directive-core` under `@deftai/directive`,
24
+ * so the old `../../../cli/dist` relative path resolved to a non-existent
25
+ * `@deftai/cli` sibling. Prefer the published package root, then monorepo path,
26
+ * then nested `@deftai/directive/dist`.
27
+ */
28
+ export declare function cliScriptPath(name: string): string;
20
29
  export interface RunProtectedCheckOptions {
21
30
  readonly nodeExecutable?: string;
22
31
  readonly timeout?: number;