@deftai/directive-core 0.89.0 → 0.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/doctor/constants.d.ts +1 -1
  2. package/dist/doctor/constants.js +2 -0
  3. package/dist/doctor/flags.js +21 -1
  4. package/dist/doctor/index.d.ts +1 -0
  5. package/dist/doctor/index.js +1 -0
  6. package/dist/doctor/main.js +22 -0
  7. package/dist/doctor/openclaw-skills.d.ts +109 -0
  8. package/dist/doctor/openclaw-skills.js +463 -0
  9. package/dist/doctor/types.d.ts +11 -0
  10. package/dist/hooks/dispatcher.js +20 -5
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/lifecycle/events.js +2 -0
  14. package/dist/lifecycle/index.d.ts +1 -0
  15. package/dist/lifecycle/index.js +1 -0
  16. package/dist/lifecycle/stats.d.ts +53 -0
  17. package/dist/lifecycle/stats.js +286 -0
  18. package/dist/release/spawn.js +13 -0
  19. package/dist/release-e2e/git-ops.d.ts +7 -0
  20. package/dist/release-e2e/git-ops.js +35 -2
  21. package/dist/session/index.d.ts +2 -0
  22. package/dist/session/index.js +2 -0
  23. package/dist/session/process-cost-constants.d.ts +9 -0
  24. package/dist/session/process-cost-constants.js +11 -0
  25. package/dist/session/process-cost.d.ts +53 -0
  26. package/dist/session/process-cost.js +82 -0
  27. package/dist/session/ritual-sentinel.d.ts +5 -0
  28. package/dist/session/ritual-sentinel.js +12 -1
  29. package/dist/session/session-ready.d.ts +67 -0
  30. package/dist/session/session-ready.js +264 -0
  31. package/dist/session/session-start.d.ts +56 -1
  32. package/dist/session/session-start.js +378 -24
  33. package/dist/session/verify-session-ritual.d.ts +8 -0
  34. package/dist/session/verify-session-ritual.js +95 -35
  35. package/dist/tool-events/classify.d.ts +20 -0
  36. package/dist/tool-events/classify.js +634 -0
  37. package/dist/tool-events/index.d.ts +10 -0
  38. package/dist/tool-events/index.js +10 -0
  39. package/dist/tool-events/summarize.d.ts +34 -0
  40. package/dist/tool-events/summarize.js +93 -0
  41. package/dist/tool-events/types.d.ts +52 -0
  42. package/dist/tool-events/types.js +13 -0
  43. package/package.json +7 -3
@@ -75,6 +75,10 @@ export interface DoctorFlags {
75
75
  readonly network: boolean;
76
76
  readonly help: boolean;
77
77
  readonly projectRoot: string | null;
78
+ /** Replace divergent OpenClaw pin dirs during doctor --fix (#3001). */
79
+ readonly force: boolean;
80
+ /** Wire always-pins into main + every workspace-* seat (#3001). */
81
+ readonly openclawAllAgents: boolean;
78
82
  readonly unknown: readonly string[];
79
83
  }
80
84
  export interface ThrottleDecision {
@@ -161,5 +165,12 @@ export interface DoctorSeams {
161
165
  readonly distance: "current" | "behind-minor" | "behind-major";
162
166
  readonly stale: boolean;
163
167
  };
168
+ /**
169
+ * OpenClaw skill-pin seams (#3001). Injected so detect/fix stays offline +
170
+ * deterministic in tests (fake HOME / env / fs).
171
+ */
172
+ readonly openclawEnv?: NodeJS.ProcessEnv;
173
+ readonly openclawHomeDir?: () => string;
174
+ readonly openclawContentRootFor?: (frameworkRoot: string) => string;
164
175
  }
165
176
  //# sourceMappingURL=types.d.ts.map
@@ -7,9 +7,10 @@ import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRE
7
7
  import { classifyMcpTool, DEFAULT_RUNTIME_AUTHORITY_POLICY, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
8
8
  import { loadStoryWriteFenceFromPath, resolveWriteFence } from "../policy/write-fence.js";
9
9
  import { detectBranch } from "../session/git.js";
10
+ import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
10
11
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
11
12
  import { runSessionStartHookWrite } from "../session/session-start-hook.js";
12
- import { inspectSessionRitual } from "../session/verify-session-ritual.js";
13
+ import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
13
14
  import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
14
15
  import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
15
16
  import { inspectActiveScope } from "./scope.js";
@@ -452,13 +453,27 @@ function inspectMutationGates(input, toolName, seams, options) {
452
453
  ((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
453
454
  }
454
455
  catch (cause) {
456
+ // #2994: best-effort local process-cost; never changes deny verdict.
457
+ emitSessionRitualBlockedProcessCost({
458
+ toolName,
459
+ code: "ritual-not-ready",
460
+ recoveryTier: "cold",
461
+ detail: `inspect threw: ${String(cause)}`,
462
+ }, { projectRoot });
455
463
  return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
456
- "Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
464
+ formatRitualRecoveryInstruction("cold"));
457
465
  }
458
466
  if (ritual.code !== 0) {
459
- return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
460
- "Recovery: run `deft session:start`, then " +
461
- "`deft verify:session-ritual -- --tier=gated`.");
467
+ // #2992: prefer re-arm recovery when age/compact stale; cold when bind invalid.
468
+ const recoveryTier = ritual.recoveryTier === "rearm" ? "rearm" : "cold";
469
+ // #2994: best-effort local process-cost; never changes deny verdict.
470
+ emitSessionRitualBlockedProcessCost({
471
+ toolName,
472
+ code: "ritual-not-ready",
473
+ recoveryTier,
474
+ detail: ritual.message,
475
+ }, { projectRoot });
476
+ return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ${formatRitualRecoveryInstruction(recoveryTier)}`);
462
477
  }
463
478
  if (options.proposedLifecycleExempt) {
464
479
  const writeTarget = hookWriteTargetPath(input.payload);
package/dist/index.d.ts CHANGED
@@ -53,6 +53,7 @@ export * as session from "./session/index.js";
53
53
  export * as slice from "./slice/index.js";
54
54
  export * as storyReady from "./story-ready/index.js";
55
55
  export * as swarm from "./swarm/index.js";
56
+ export * as toolEvents from "./tool-events/index.js";
56
57
  export * as triage from "./triage/index.js";
57
58
  export * as userConfig from "./user-config/index.js";
58
59
  export * as validateContent from "./validate-content/index.js";
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ export * as session from "./session/index.js";
54
54
  export * as slice from "./slice/index.js";
55
55
  export * as storyReady from "./story-ready/index.js";
56
56
  export * as swarm from "./swarm/index.js";
57
+ export * as toolEvents from "./tool-events/index.js";
57
58
  export * as triage from "./triage/index.js";
58
59
  export * as userConfig from "./user-config/index.js";
59
60
  export * as validateContent from "./validate-content/index.js";
@@ -6,11 +6,13 @@ import { contentRoot } from "../content-root.js";
6
6
  import { ATTRIBUTION_REQUIRED_PAYLOAD } from "../events/attribution-constants.js";
7
7
  import { containedWrite } from "../fs/contained-write.js";
8
8
  import { ProjectionContainmentError } from "../fs/projection-containment.js";
9
+ import { PROCESS_COST_REQUIRED_PAYLOAD } from "../session/process-cost-constants.js";
9
10
  /** Default event log location (project-local). */
10
11
  export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
11
12
  const BEHAVIORAL_CATEGORY = "behavioral";
12
13
  const REQUIRED_BEHAVIORAL_PAYLOAD = {
13
14
  ...ATTRIBUTION_REQUIRED_PAYLOAD,
15
+ ...PROCESS_COST_REQUIRED_PAYLOAD,
14
16
  "session:interrupted": ["session_id", "reason"],
15
17
  "session:resumed": ["session_id", "interrupted_id"],
16
18
  "plan:approved": ["plan_ref", "approver"],
@@ -2,4 +2,5 @@ export * from "./event.js";
2
2
  export * as eventDetect from "./event-detect.js";
3
3
  export * as events from "./events.js";
4
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
5
+ export * from "./stats.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -2,4 +2,5 @@ export * from "./event.js";
2
2
  export * as eventDetect from "./event-detect.js";
3
3
  export * as events from "./events.js";
4
4
  export * as lifecycleHygiene from "./lifecycle-hygiene.js";
5
+ export * from "./stats.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,53 @@
1
+ import { parseDurationMs } from "../triage/scope/duration.js";
2
+ /** Lifecycle folders scanned for stats. */
3
+ export declare const STATS_LIFECYCLE_FOLDERS: readonly ["proposed", "pending", "active", "completed", "cancelled"];
4
+ export type StatsLifecycleFolder = (typeof STATS_LIFECYCLE_FOLDERS)[number];
5
+ /** Count semantics for WWYSYDH / process forms (stable strings for --json). */
6
+ export declare const LIFECYCLE_STATS_SEMANTICS: {
7
+ readonly promoted: string;
8
+ readonly activated: string;
9
+ readonly completed: string;
10
+ readonly cancelled_or_failed: string;
11
+ readonly still_active: "snapshot count of all xbriefs currently in active/ (not filtered by --since).";
12
+ readonly event_time: string;
13
+ readonly note: string;
14
+ };
15
+ export interface LifecycleFolderTotals {
16
+ readonly proposed: number;
17
+ readonly pending: number;
18
+ readonly active: number;
19
+ readonly completed: number;
20
+ readonly cancelled: number;
21
+ }
22
+ export interface LifecycleStats {
23
+ readonly since: string;
24
+ readonly since_ms: number;
25
+ readonly as_of: string;
26
+ readonly window_start: string;
27
+ readonly project_root: string;
28
+ readonly lifecycle_root: string;
29
+ readonly promoted: number;
30
+ readonly activated: number;
31
+ readonly completed: number;
32
+ readonly cancelled_or_failed: number;
33
+ readonly still_active: number;
34
+ readonly folder_totals: LifecycleFolderTotals;
35
+ readonly semantics: typeof LIFECYCLE_STATS_SEMANTICS;
36
+ }
37
+ export interface CollectLifecycleStatsOptions {
38
+ readonly projectRoot: string;
39
+ /** Window duration string, e.g. "7d", "24h", "1w". Default "7d". */
40
+ readonly since?: string;
41
+ /** Clock for window end (tests inject a fixed Date). */
42
+ readonly now?: Date;
43
+ }
44
+ /**
45
+ * Collect lifecycle folder stats for a project root.
46
+ * Offline / filesystem only.
47
+ */
48
+ export declare function collectLifecycleStats(options: CollectLifecycleStatsOptions): LifecycleStats;
49
+ /** Format human-readable lifecycle stats text. */
50
+ export declare function formatLifecycleStatsText(stats: LifecycleStats): string;
51
+ /** Re-export duration parse for CLI error messaging. */
52
+ export { parseDurationMs };
53
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Local xBRIEF lifecycle folder stats for weekly process rollups (#2995).
3
+ *
4
+ * Filesystem-only inventory of `xbrief/{proposed,pending,active,completed,cancelled}/`.
5
+ * No network calls.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { join, resolve } from "node:path";
9
+ import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
10
+ import { parseDurationMs } from "../triage/scope/duration.js";
11
+ /** Lifecycle folders scanned for stats. */
12
+ export const STATS_LIFECYCLE_FOLDERS = [
13
+ "proposed",
14
+ "pending",
15
+ "active",
16
+ "completed",
17
+ "cancelled",
18
+ ];
19
+ /** Count semantics for WWYSYDH / process forms (stable strings for --json). */
20
+ export const LIFECYCLE_STATS_SEMANTICS = {
21
+ promoted: "xbriefs currently in pending/ whose event time falls inside the --since window " +
22
+ "(promoted and still pending; later-activated work is not double-counted here).",
23
+ activated: "xbriefs currently in active/ whose event time falls inside the --since window " +
24
+ "(activated and still active).",
25
+ completed: "xbriefs currently in completed/ with plan.status completed (or unset, treated as completed) " +
26
+ "whose event time falls inside the --since window.",
27
+ cancelled_or_failed: "xbriefs currently in cancelled/, or in completed/ with plan.status failed, " +
28
+ "whose event time falls inside the --since window.",
29
+ still_active: "snapshot count of all xbriefs currently in active/ (not filtered by --since).",
30
+ event_time: "event time = most recent of plan.metadata.completedAt, plan.updated, and xBRIEFInfo/vBRIEFInfo.updated; " +
31
+ "else file mtime. Window is [as_of - since, as_of] inclusive of as_of.",
32
+ note: "Counts are current-folder membership, not full transition history. A brief that was " +
33
+ "promoted then activated in the same week appears under activated (and still_active), not promoted.",
34
+ };
35
+ function parseIso(value) {
36
+ if (typeof value !== "string" || value.trim().length === 0) {
37
+ return null;
38
+ }
39
+ const text = value.trim().endsWith("Z") ? `${value.trim().slice(0, -1)}+00:00` : value.trim();
40
+ const parsed = new Date(text);
41
+ if (Number.isNaN(parsed.getTime())) {
42
+ return null;
43
+ }
44
+ return parsed;
45
+ }
46
+ function readJsonObject(path) {
47
+ try {
48
+ const raw = readFileSync(path, "utf8");
49
+ const data = JSON.parse(raw);
50
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
51
+ return null;
52
+ }
53
+ return data;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function planOf(data) {
60
+ const plan = data.plan;
61
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
62
+ return null;
63
+ }
64
+ return plan;
65
+ }
66
+ function infoUpdated(data) {
67
+ for (const key of ["xBRIEFInfo", "vBRIEFInfo"]) {
68
+ const info = data[key];
69
+ if (typeof info === "object" && info !== null && !Array.isArray(info)) {
70
+ const stamp = parseIso(info.updated);
71
+ if (stamp !== null) {
72
+ return stamp;
73
+ }
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ function completedAt(plan) {
79
+ const metadata = plan.metadata;
80
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) {
81
+ return null;
82
+ }
83
+ return parseIso(metadata.completedAt);
84
+ }
85
+ /**
86
+ * Event time for window membership: most recent of completedAt / plan.updated /
87
+ * info.updated, else mtime. Preferring max (not completedAt-first) so a later
88
+ * cancel/restore transition is not stuck on a stale completion stamp.
89
+ */
90
+ function eventTimeFor(data, plan, path) {
91
+ const candidates = [];
92
+ const completed = completedAt(plan);
93
+ if (completed !== null) {
94
+ candidates.push(completed);
95
+ }
96
+ const planUpdated = parseIso(plan.updated);
97
+ if (planUpdated !== null) {
98
+ candidates.push(planUpdated);
99
+ }
100
+ const info = infoUpdated(data);
101
+ if (info !== null) {
102
+ candidates.push(info);
103
+ }
104
+ if (candidates.length > 0) {
105
+ return new Date(Math.max(...candidates.map((d) => d.getTime())));
106
+ }
107
+ try {
108
+ return new Date(statSync(path).mtimeMs);
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /** Stats root: xbrief/ if present, else legacy vbrief/ (read-only inventory), else canonical xbrief path. */
115
+ function resolveStatsLifecycleRoot(projectRoot) {
116
+ const migrated = join(projectRoot, MIGRATED_ARTIFACT_DIR);
117
+ if (existsSync(migrated)) {
118
+ try {
119
+ if (statSync(migrated).isDirectory()) {
120
+ return migrated;
121
+ }
122
+ }
123
+ catch {
124
+ // fall through
125
+ }
126
+ }
127
+ const legacy = join(projectRoot, LEGACY_ARTIFACT_DIR);
128
+ if (existsSync(legacy)) {
129
+ try {
130
+ if (statSync(legacy).isDirectory()) {
131
+ return legacy;
132
+ }
133
+ }
134
+ catch {
135
+ // fall through
136
+ }
137
+ }
138
+ return migrated;
139
+ }
140
+ function statusOf(plan, folder) {
141
+ const raw = plan.status;
142
+ if (typeof raw === "string" && raw.trim().length > 0) {
143
+ return raw.trim();
144
+ }
145
+ if (folder === "completed") {
146
+ return "completed";
147
+ }
148
+ if (folder === "cancelled") {
149
+ return "cancelled";
150
+ }
151
+ if (folder === "pending") {
152
+ return "pending";
153
+ }
154
+ if (folder === "active") {
155
+ return "running";
156
+ }
157
+ return "proposed";
158
+ }
159
+ function inWindow(eventTime, windowStart, asOf) {
160
+ if (eventTime === null) {
161
+ return false;
162
+ }
163
+ const t = eventTime.getTime();
164
+ return t >= windowStart.getTime() && t <= asOf.getTime();
165
+ }
166
+ function emptyFolderTotals() {
167
+ return { proposed: 0, pending: 0, active: 0, completed: 0, cancelled: 0 };
168
+ }
169
+ function scanArtifacts(lifecycleRoot) {
170
+ const out = [];
171
+ for (const folder of STATS_LIFECYCLE_FOLDERS) {
172
+ const dir = join(lifecycleRoot, folder);
173
+ if (!existsSync(dir)) {
174
+ continue;
175
+ }
176
+ let names;
177
+ try {
178
+ names = readdirSync(dir)
179
+ .filter((name) => hasArtifactSuffix(name))
180
+ .sort();
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ for (const name of names) {
186
+ const path = join(dir, name);
187
+ const data = readJsonObject(path);
188
+ if (data === null) {
189
+ continue;
190
+ }
191
+ const plan = planOf(data);
192
+ if (plan === null) {
193
+ continue;
194
+ }
195
+ out.push({
196
+ folder,
197
+ status: statusOf(plan, folder),
198
+ eventTime: eventTimeFor(data, plan, path),
199
+ });
200
+ }
201
+ }
202
+ return out;
203
+ }
204
+ function utcIso(dt) {
205
+ return `${dt.toISOString().slice(0, 19)}Z`;
206
+ }
207
+ /**
208
+ * Collect lifecycle folder stats for a project root.
209
+ * Offline / filesystem only.
210
+ */
211
+ export function collectLifecycleStats(options) {
212
+ const sinceRaw = (options.since ?? "7d").trim() || "7d";
213
+ const sinceMs = parseDurationMs(sinceRaw);
214
+ const asOf = options.now ?? new Date();
215
+ const windowStart = new Date(asOf.getTime() - sinceMs);
216
+ const projectRoot = resolve(options.projectRoot);
217
+ const lifecycleRoot = resolveStatsLifecycleRoot(projectRoot);
218
+ const folder_totals = emptyFolderTotals();
219
+ let promoted = 0;
220
+ let activated = 0;
221
+ let completed = 0;
222
+ let cancelled_or_failed = 0;
223
+ let still_active = 0;
224
+ const records = existsSync(lifecycleRoot) ? scanArtifacts(lifecycleRoot) : [];
225
+ for (const rec of records) {
226
+ folder_totals[rec.folder] += 1;
227
+ if (rec.folder === "active") {
228
+ still_active += 1;
229
+ }
230
+ const win = inWindow(rec.eventTime, windowStart, asOf);
231
+ if (!win) {
232
+ continue;
233
+ }
234
+ if (rec.folder === "pending") {
235
+ promoted += 1;
236
+ }
237
+ else if (rec.folder === "active") {
238
+ activated += 1;
239
+ }
240
+ else if (rec.folder === "cancelled") {
241
+ cancelled_or_failed += 1;
242
+ }
243
+ else if (rec.folder === "completed") {
244
+ if (rec.status === "failed") {
245
+ cancelled_or_failed += 1;
246
+ }
247
+ else if (rec.status === "completed") {
248
+ completed += 1;
249
+ }
250
+ // Inconsistent statuses under completed/ (e.g. stale "running") are
251
+ // counted in folder_totals only — not in window metrics.
252
+ }
253
+ }
254
+ return {
255
+ since: sinceRaw,
256
+ since_ms: sinceMs,
257
+ as_of: utcIso(asOf),
258
+ window_start: utcIso(windowStart),
259
+ project_root: projectRoot,
260
+ lifecycle_root: lifecycleRoot,
261
+ promoted,
262
+ activated,
263
+ completed,
264
+ cancelled_or_failed,
265
+ still_active,
266
+ folder_totals: { ...folder_totals },
267
+ semantics: LIFECYCLE_STATS_SEMANTICS,
268
+ };
269
+ }
270
+ /** Format human-readable lifecycle stats text. */
271
+ export function formatLifecycleStatsText(stats) {
272
+ const lines = [
273
+ `lifecycle:stats (since ${stats.since}, as of ${stats.as_of})`,
274
+ ` window: ${stats.window_start} → ${stats.as_of}`,
275
+ ` promoted: ${stats.promoted}`,
276
+ ` activated: ${stats.activated}`,
277
+ ` completed: ${stats.completed}`,
278
+ ` cancelled_or_failed: ${stats.cancelled_or_failed}`,
279
+ ` still_active: ${stats.still_active}`,
280
+ ` folder_totals: proposed=${stats.folder_totals.proposed} pending=${stats.folder_totals.pending} active=${stats.folder_totals.active} completed=${stats.folder_totals.completed} cancelled=${stats.folder_totals.cancelled}`,
281
+ ];
282
+ return `${lines.join("\n")}\n`;
283
+ }
284
+ /** Re-export duration parse for CLI error messaging. */
285
+ export { parseDurationMs };
286
+ //# sourceMappingURL=stats.js.map
@@ -24,6 +24,19 @@ export function spawnText(cmd, args, options = {}) {
24
24
  if (status === null) {
25
25
  if (result.signal !== null && result.signal !== undefined) {
26
26
  status = 128;
27
+ // Timeout kills leave stderr empty; surface timeout so e2e never reports
28
+ // "failed: " with a blank reason (#3004 / #1867).
29
+ if (stderr.trim().length === 0) {
30
+ if (result.error) {
31
+ stderr = result.error.message;
32
+ }
33
+ else if (options.timeoutMs !== undefined) {
34
+ stderr = `process terminated by signal ${result.signal} (timeout ${options.timeoutMs}ms)`;
35
+ }
36
+ else {
37
+ stderr = `process terminated by signal ${result.signal}`;
38
+ }
39
+ }
27
40
  }
28
41
  else if (result.error) {
29
42
  status = 2;
@@ -1,4 +1,11 @@
1
+ import type { SpawnResult } from "../release/types.js";
1
2
  import type { E2ESeams } from "./types.js";
3
+ /** Mirror push can exceed the generic release `runGit` 30s cap (#3004). Align with clone. */
4
+ export declare const PUSH_MIRROR_TIMEOUT_MS = 300000;
5
+ /** Prefer stderr; never leave a blank reason after "failed:" (#3004). */
6
+ export declare function formatGitOpFailure(op: string, result: SpawnResult, options?: {
7
+ timeoutMs?: number;
8
+ }): string;
2
9
  export declare function cloneRepoToTemp(projectRoot: string, targetDir: string, seams?: E2ESeams): [boolean, string];
3
10
  export declare function setOriginToTempRepo(cloneDir: string, owner: string, slug: string, seams?: E2ESeams): [boolean, string];
4
11
  export declare function pushMirror(cloneDir: string, seams?: E2ESeams): [boolean, string];
@@ -1,11 +1,22 @@
1
1
  import { runGit as releaseRunGit } from "../release/git.js";
2
2
  import { spawnText } from "../release/spawn.js";
3
+ /** Mirror push can exceed the generic release `runGit` 30s cap (#3004). Align with clone. */
4
+ export const PUSH_MIRROR_TIMEOUT_MS = 300_000;
3
5
  function defaultRunGit(projectRoot, args, env, seams = {}) {
4
6
  if (seams.runGit) {
5
7
  return seams.runGit(projectRoot, args, env);
6
8
  }
7
9
  return releaseRunGit(projectRoot, args, { spawnText: seams.spawnText ?? spawnText }, env);
8
10
  }
11
+ /** Prefer stderr; never leave a blank reason after "failed:" (#3004). */
12
+ export function formatGitOpFailure(op, result, options = {}) {
13
+ const stderr = result.stderr.trim();
14
+ if (stderr.length > 0) {
15
+ return `${op} failed: ${stderr}`;
16
+ }
17
+ const timeoutHint = options.timeoutMs !== undefined ? `; possible timeout after ${options.timeoutMs}ms` : "";
18
+ return `${op} failed: no stderr (exit ${result.status ?? "null"}${timeoutHint})`;
19
+ }
9
20
  export function cloneRepoToTemp(projectRoot, targetDir, seams = {}) {
10
21
  const env = { ...process.env, DEFT_PROJECT_ROOT: targetDir };
11
22
  const spawn = seams.spawnText ?? spawnText;
@@ -27,9 +38,31 @@ export function setOriginToTempRepo(cloneDir, owner, slug, seams = {}) {
27
38
  return [true, `origin -> ${url}`];
28
39
  }
29
40
  export function pushMirror(cloneDir, seams = {}) {
30
- const result = defaultRunGit(cloneDir, ["push", "origin", "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"], undefined, seams);
41
+ // Do not use default release runGit (30s) full heads+tags mirror of directive
42
+ // routinely exceeds that on first push to an empty temp repo (#3004).
43
+ const pushArgs = [
44
+ "push",
45
+ "origin",
46
+ "refs/heads/*:refs/heads/*",
47
+ "refs/tags/*:refs/tags/*",
48
+ ];
49
+ let result;
50
+ if (seams.runGit) {
51
+ result = seams.runGit(cloneDir, [...pushArgs], undefined);
52
+ }
53
+ else {
54
+ const spawn = seams.spawnText ?? spawnText;
55
+ result = spawn("git", ["-C", cloneDir, ...pushArgs], {
56
+ timeoutMs: PUSH_MIRROR_TIMEOUT_MS,
57
+ });
58
+ }
31
59
  if (result.status !== 0) {
32
- return [false, `git push (heads+tags refspecs) failed: ${result.stderr.trim()}`];
60
+ return [
61
+ false,
62
+ formatGitOpFailure("git push (heads+tags refspecs)", result, {
63
+ timeoutMs: PUSH_MIRROR_TIMEOUT_MS,
64
+ }),
65
+ ];
33
66
  }
34
67
  return [true, "pushed heads + tags to temp origin"];
35
68
  }
@@ -1,8 +1,10 @@
1
1
  export * from "./git.js";
2
2
  export * from "./json.js";
3
3
  export * from "./posture.js";
4
+ export * from "./process-cost.js";
4
5
  export * from "./resume-conditions.js";
5
6
  export * from "./ritual-sentinel.js";
7
+ export * from "./session-ready.js";
6
8
  export * from "./session-start.js";
7
9
  export * from "./session-start-hook.js";
8
10
  export * from "./time.js";
@@ -1,8 +1,10 @@
1
1
  export * from "./git.js";
2
2
  export * from "./json.js";
3
3
  export * from "./posture.js";
4
+ export * from "./process-cost.js";
4
5
  export * from "./resume-conditions.js";
5
6
  export * from "./ritual-sentinel.js";
7
+ export * from "./session-ready.js";
6
8
  export * from "./session-start.js";
7
9
  export * from "./session-start-hook.js";
8
10
  export * from "./time.js";
@@ -0,0 +1,9 @@
1
+ /** Canonical process-cost event names for ceremony observability (#2994). */
2
+ export declare const PROCESS_COST_EVENT_NAMES: {
3
+ readonly sessionStart: "session:start";
4
+ readonly sessionRitualBlocked: "session:ritual-blocked";
5
+ };
6
+ export type ProcessCostEventName = (typeof PROCESS_COST_EVENT_NAMES)[keyof typeof PROCESS_COST_EVENT_NAMES];
7
+ /** Required payload keys per process-cost event (merged into lifecycle/events). */
8
+ export declare const PROCESS_COST_REQUIRED_PAYLOAD: Readonly<Record<string, readonly string[]>>;
9
+ //# sourceMappingURL=process-cost-constants.d.ts.map
@@ -0,0 +1,11 @@
1
+ /** Canonical process-cost event names for ceremony observability (#2994). */
2
+ export const PROCESS_COST_EVENT_NAMES = {
3
+ sessionStart: "session:start",
4
+ sessionRitualBlocked: "session:ritual-blocked",
5
+ };
6
+ /** Required payload keys per process-cost event (merged into lifecycle/events). */
7
+ export const PROCESS_COST_REQUIRED_PAYLOAD = {
8
+ [PROCESS_COST_EVENT_NAMES.sessionStart]: ["ceremony_tier", "duration_ms", "exit_code"],
9
+ [PROCESS_COST_EVENT_NAMES.sessionRitualBlocked]: ["tool_name", "code"],
10
+ };
11
+ //# sourceMappingURL=process-cost-constants.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Local process-cost ceremony events (#2994).
3
+ *
4
+ * Always-on, best-effort appends to `.deft-cache/events.jsonl`.
5
+ * Not gated on valueFeedback (distinct from #1709 attribution).
6
+ * No remote / Product Insights upload (#2603 not required).
7
+ *
8
+ * Types are declared locally (not imported from session-start) so this module
9
+ * does not form an import cycle with session-start call sites.
10
+ */
11
+ import { type BehavioralEventRecord } from "../lifecycle/events.js";
12
+ export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, type ProcessCostEventName, } from "./process-cost-constants.js";
13
+ /** Ceremony tier labels mirror session-start cold|rearm (#2992 / #2994). */
14
+ export type ProcessCostCeremonyTier = "cold" | "rearm";
15
+ export interface ProcessCostStepTiming {
16
+ readonly name: string;
17
+ readonly duration_ms: number;
18
+ readonly skipped?: boolean;
19
+ }
20
+ export interface EmitProcessCostOptions {
21
+ readonly projectRoot: string;
22
+ /**
23
+ * Optional explicit log path for tests.
24
+ * When omitted, `emit` resolves via DEFT_EVENT_LOG then `.deft-cache/events.jsonl`
25
+ * (must not pre-resolve the default here or DEFT_EVENT_LOG is bypassed).
26
+ */
27
+ readonly logPath?: string | null;
28
+ }
29
+ export interface SessionStartProcessCostInput {
30
+ readonly ceremonyTier: ProcessCostCeremonyTier;
31
+ readonly durationMs: number;
32
+ readonly exitCode: number;
33
+ readonly ready?: boolean;
34
+ readonly optionalNetwork?: boolean;
35
+ readonly steps?: readonly ProcessCostStepTiming[];
36
+ }
37
+ export interface SessionRitualBlockedProcessCostInput {
38
+ readonly toolName: string;
39
+ readonly code?: string;
40
+ readonly recoveryTier?: "cold" | "rearm";
41
+ readonly detail?: string;
42
+ }
43
+ /**
44
+ * Emit `session:start` after cold/re-arm ceremony completes (or fails early).
45
+ * Returns null on any failure (telemetry must not interrupt session:start).
46
+ */
47
+ export declare function emitSessionStartProcessCost(input: SessionStartProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
48
+ /**
49
+ * Emit `session:ritual-blocked` on PreToolUse ritual-not-ready deny.
50
+ * Returns null on any failure (telemetry must not change deny verdict).
51
+ */
52
+ export declare function emitSessionRitualBlockedProcessCost(input: SessionRitualBlockedProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
53
+ //# sourceMappingURL=process-cost.d.ts.map