@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
@@ -8,21 +8,37 @@ import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRE
8
8
  import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
9
9
  import { resolvePolicy } from "../policy/resolve.js";
10
10
  import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
11
+ import { formatFrameworkCommand } from "../render/framework-commands.js";
11
12
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
12
13
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
13
14
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
14
15
  import { emitSessionValueReadback } from "../value/readback.js";
15
16
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
16
- import { defaultGitRunner, gitHead, worktreePath } from "./git.js";
17
+ import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
18
+ import { emitSessionStartProcessCost } from "./process-cost.js";
17
19
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
18
- import { newRitualStatePayload, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
20
+ import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
19
21
  export const SESSION_POSTURES = ["read-only", "mutation"];
20
22
  export const READ_ONLY_POSTURE = "read-only";
21
23
  export const MUTATION_POSTURE = "mutation";
22
24
  export const READ_ONLY_ALIGNMENT_MESSAGE = "Deft Directive active -- AGENTS.md loaded.";
23
25
  export const READ_ONLY_RESULT_MESSAGE = "read-only session posture (alignment only; no ritual-state write)";
26
+ /** Cold (full) vs re-arm (clock/HEAD refresh) ceremony tiers (#2992). */
27
+ export const SESSION_CEREMONY_TIERS = ["cold", "rearm"];
28
+ export const COLD_CEREMONY_TIER = "cold";
29
+ export const REARM_CEREMONY_TIER = "rearm";
24
30
  export const QUICK_STEPS = ["alignment", "branch_policy", "triage_welcome"];
25
31
  export const GATED_STEPS = ["doctor", "cache_fresh"];
32
+ /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
33
+ export const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
34
+ /** Human-readable skip notice when optional network is off the hot path (#2991). */
35
+ export const OPTIONAL_NETWORK_SKIPPED_MESSAGE = "[deft session] optional network skipped (release probe, triage cache hydrate/self-heal); " +
36
+ "pass --with-network or set DEFT_SESSION_START_NETWORK=1 to enable.";
37
+ /** Re-arm skips fat cold-path work when prior ritual is still structurally valid (#2992). */
38
+ export const REARM_SKIPPED_FAT_PATH_MESSAGE = "[deft session] re-arm tier: refreshed ritual clock + HEAD/worktree bind; " +
39
+ "skipped verify:tools, triage welcome, release probe, and staleness tickler.";
40
+ /** When --rearm is requested but state requires a full cold ceremony (#2992). */
41
+ export const REARM_INELIGIBLE_PREFIX = "session re-arm refused — full cold session:start required:";
26
42
  const STEP_ALIASES = {
27
43
  branch: "branch_policy",
28
44
  "branch-policy": "branch_policy",
@@ -31,6 +47,89 @@ const STEP_ALIASES = {
31
47
  triage: "triage_welcome",
32
48
  "triage-welcome": "triage_welcome",
33
49
  };
50
+ /** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
51
+ export function formatSessionStartRecoveryCommand(tier = "cold") {
52
+ if (tier === "rearm") {
53
+ return formatFrameworkCommand(["session:start", "--rearm"]);
54
+ }
55
+ return formatFrameworkCommand(["session:start"]);
56
+ }
57
+ function stepPassesForRearm(step) {
58
+ if (!step || typeof step !== "object")
59
+ return false;
60
+ if (step.deferred_reason)
61
+ return true;
62
+ return step.ok === true;
63
+ }
64
+ /**
65
+ * Whether a prior ritual can be re-armed without a full cold ceremony (#2992).
66
+ * Requires valid state, same worktree, continuous (or identical) HEAD, and
67
+ * previously-passing quick steps.
68
+ */
69
+ export function assessRearmEligibility(projectRoot, options = {}) {
70
+ const runGit = options.runGit ?? defaultGitRunner;
71
+ const [state, err] = readRitualState(projectRoot);
72
+ if (state === null) {
73
+ return {
74
+ eligible: false,
75
+ reason: err ?? "ritual state missing (first install / cold path required)",
76
+ };
77
+ }
78
+ const { head: currentHead, error: headError } = gitHead(projectRoot, runGit);
79
+ if (currentHead === null) {
80
+ return { eligible: false, reason: headError ?? "could not resolve git HEAD" };
81
+ }
82
+ const currentWorktree = worktreePath(projectRoot, runGit);
83
+ if (state.worktreePath !== currentWorktree) {
84
+ return {
85
+ eligible: false,
86
+ reason: `ritual state belongs to a different worktree (${state.worktreePath})`,
87
+ };
88
+ }
89
+ if (state.gitHead !== currentHead) {
90
+ const forward = gitIsAncestor(projectRoot, state.gitHead, currentHead, runGit);
91
+ if (forward === null) {
92
+ return { eligible: false, reason: "could not verify git history for session re-arm" };
93
+ }
94
+ if (!forward) {
95
+ return {
96
+ eligible: false,
97
+ reason: "git HEAD changed discontinuously (full cold session:start required)",
98
+ };
99
+ }
100
+ }
101
+ for (const stepName of QUICK_STEPS) {
102
+ if (!stepPassesForRearm(state.quickSteps[stepName])) {
103
+ return {
104
+ eligible: false,
105
+ reason: `quick step '${stepName}' is missing or failed (full cold session:start required)`,
106
+ };
107
+ }
108
+ }
109
+ return { eligible: true, state, currentHead, currentWorktree };
110
+ }
111
+ function restampSteps(steps, now) {
112
+ const out = {};
113
+ for (const [name, step] of Object.entries(steps)) {
114
+ out[name] = {
115
+ ...step,
116
+ ts: ritualStep({ ok: step.ok === true, ts: now }).ts,
117
+ };
118
+ }
119
+ return out;
120
+ }
121
+ /** Resolve whether optional session:start network work is enabled (#2991). */
122
+ export function resolveSessionStartOptionalNetwork(options = {}) {
123
+ if (options.allowOptionalNetwork === true)
124
+ return true;
125
+ if (options.allowOptionalNetwork === false)
126
+ return false;
127
+ const env = options.env ?? process.env;
128
+ return env[ENV_SESSION_START_NETWORK] === "1";
129
+ }
130
+ function elapsedMs(started) {
131
+ return Math.max(0, Math.round(performance.now() - started));
132
+ }
34
133
  function normaliseStepName(name) {
35
134
  return STEP_ALIASES[name] ?? name;
36
135
  }
@@ -229,12 +328,174 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
229
328
  };
230
329
  return { code: 0, payload: resultPayload, lines };
231
330
  }
331
+ function runSessionRearm(projectRoot, options, instant, environment) {
332
+ const overallStarted = performance.now();
333
+ const runGit = options.runGit ?? defaultGitRunner;
334
+ const eligibility = assessRearmEligibility(projectRoot, { runGit });
335
+ if (!eligibility.eligible) {
336
+ const coldCmd = formatSessionStartRecoveryCommand("cold");
337
+ const message = `${REARM_INELIGIBLE_PREFIX} ${eligibility.reason}. Run \`${coldCmd}\`.`;
338
+ // #2994: still record failed attempt duration for process-cost rollups.
339
+ emitSessionStartProcessCost({
340
+ ceremonyTier: REARM_CEREMONY_TIER,
341
+ durationMs: elapsedMs(overallStarted),
342
+ exitCode: 1,
343
+ ready: false,
344
+ optionalNetwork: false,
345
+ }, { projectRoot });
346
+ return {
347
+ code: 1,
348
+ payload: {
349
+ ready: false,
350
+ exit_code: 1,
351
+ ceremony_tier: REARM_CEREMONY_TIER,
352
+ rearm_eligible: false,
353
+ environment: environmentContextToDict(environment),
354
+ message,
355
+ },
356
+ lines: [formatEnvironmentContext(environment), message],
357
+ };
358
+ }
359
+ const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
360
+ const userMd = resolveUserMd(projectRoot);
361
+ const safePath = userMd.path.replace(/\r?\n/g, " ");
362
+ const safeDiagnostic = userMd.diagnostic.replace(/\r?\n/g, " ");
363
+ const userMdLine = userMd.found
364
+ ? `USER.md resolved (${userMd.rung}): ${safePath}`
365
+ : safeDiagnostic;
366
+ const alignmentMessage = `${READ_ONLY_ALIGNMENT_MESSAGE} ${userMdLine}`;
367
+ const lines = [
368
+ READ_ONLY_ALIGNMENT_MESSAGE,
369
+ userMdLine,
370
+ formatEnvironmentContext(environment),
371
+ REARM_SKIPPED_FAT_PATH_MESSAGE,
372
+ ];
373
+ // Light branch-policy disclosure (local only) so re-arm still surfaces policy state.
374
+ const policyResult = resolvePolicy(projectRoot);
375
+ const policyMessage = disclosureLine(policyResult);
376
+ lines.push(policyMessage);
377
+ const humanMerge = resolveHumanMergePolicy(projectRoot);
378
+ const humanMergeLine = humanMergeDisclosureLine(humanMerge);
379
+ if (humanMergeLine !== null) {
380
+ lines.push(humanMergeLine);
381
+ }
382
+ const priorQuick = eligibility.state.quickSteps;
383
+ const priorTriage = priorQuick.triage_welcome ?? ritualStep({ ok: true, ts: instant });
384
+ const policyOk = policyResult.error === null || policyResult.source === "default-fail-closed";
385
+ const quickSteps = {
386
+ alignment: ritualStep({
387
+ ok: true,
388
+ ts: instant,
389
+ message: alignmentMessage,
390
+ durationMs: 0,
391
+ }),
392
+ branch_policy: ritualStep({
393
+ ok: policyOk,
394
+ ts: instant,
395
+ message: policyMessage,
396
+ exitCode: policyOk ? 0 : 2,
397
+ durationMs: 0,
398
+ }),
399
+ // Preserve prior triage outcome; do not re-run welcome / self-heal on re-arm.
400
+ triage_welcome: {
401
+ ...priorTriage,
402
+ ts: ritualStep({ ok: priorTriage.ok === true, ts: instant }).ts,
403
+ message: typeof priorTriage.message === "string"
404
+ ? priorTriage.message
405
+ : "triage welcome preserved on re-arm",
406
+ },
407
+ };
408
+ const gatedSteps = restampSteps(eligibility.state.gatedSteps, instant);
409
+ const writeStarted = performance.now();
410
+ // Fresh payload (no rearm_needed / compact_resume_at) clears compact markers (#2992).
411
+ const writePayload = {
412
+ ...newRitualStatePayload({
413
+ sessionId: (options.newSessionId ?? randomUUID)(),
414
+ gitHead: eligibility.currentHead,
415
+ worktreePath: eligibility.currentWorktree,
416
+ startedAt: instant,
417
+ quickSteps,
418
+ gatedSteps,
419
+ }),
420
+ ceremony_tier: REARM_CEREMONY_TIER,
421
+ };
422
+ let statePath;
423
+ try {
424
+ statePath = writeRitualState(projectRoot, writePayload);
425
+ }
426
+ catch (cause) {
427
+ // #2994: still record failed attempt when ritual-state write throws.
428
+ emitSessionStartProcessCost({
429
+ ceremonyTier: REARM_CEREMONY_TIER,
430
+ durationMs: elapsedMs(overallStarted),
431
+ exitCode: 2,
432
+ ready: false,
433
+ optionalNetwork: false,
434
+ steps: [
435
+ { name: "alignment", duration_ms: 0 },
436
+ { name: "branch_policy", duration_ms: 0 },
437
+ { name: "verify_tools", duration_ms: 0, skipped: true },
438
+ { name: "triage_welcome", duration_ms: 0, skipped: true },
439
+ { name: "release_probe", duration_ms: 0, skipped: true },
440
+ { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
441
+ ],
442
+ }, { projectRoot });
443
+ throw cause;
444
+ }
445
+ const stepTimings = [
446
+ { name: "alignment", duration_ms: 0 },
447
+ { name: "branch_policy", duration_ms: 0 },
448
+ { name: "verify_tools", duration_ms: 0, skipped: true },
449
+ { name: "triage_welcome", duration_ms: 0, skipped: true },
450
+ { name: "release_probe", duration_ms: 0, skipped: true },
451
+ { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
452
+ ];
453
+ const totalMs = elapsedMs(overallStarted);
454
+ const failed = Object.entries(quickSteps)
455
+ .filter(([, step]) => !step.ok && !step.deferred_reason)
456
+ .map(([name]) => name);
457
+ const code = failed.length > 0 ? 1 : 0;
458
+ // #2994: local process-cost event (best-effort; never blocks ceremony).
459
+ emitSessionStartProcessCost({
460
+ ceremonyTier: REARM_CEREMONY_TIER,
461
+ durationMs: totalMs,
462
+ exitCode: code,
463
+ ready: code === 0,
464
+ optionalNetwork: false,
465
+ steps: stepTimings,
466
+ }, { projectRoot });
467
+ return {
468
+ code,
469
+ payload: {
470
+ ready: code === 0,
471
+ exit_code: code,
472
+ ceremony_tier: REARM_CEREMONY_TIER,
473
+ rearm_eligible: true,
474
+ state_path: statePath,
475
+ quick_steps: quickSteps,
476
+ gated_steps: gatedSteps,
477
+ steps: stepTimings,
478
+ duration_ms: totalMs,
479
+ optional_network: false,
480
+ user_md: {
481
+ path: userMd.path,
482
+ rung: userMd.rung,
483
+ found: userMd.found,
484
+ diagnostic: userMd.diagnostic,
485
+ },
486
+ environment: environmentContextToDict(environment),
487
+ message: code === 0 ? "session ritual re-armed" : "session ritual re-arm failed",
488
+ },
489
+ lines,
490
+ };
491
+ }
232
492
  export function runSessionStart(projectRoot, options = {}) {
233
493
  const posture = options.posture ?? MUTATION_POSTURE;
234
494
  const instant = options.now ?? new Date();
235
495
  const deferrals = options.deferrals ?? {};
236
496
  const runGit = options.runGit ?? defaultGitRunner;
237
497
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
498
+ const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
238
499
  // #2926: official root opt-out wins locally — skip Directive session ritual.
239
500
  // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
240
501
  // automation does not treat opt-out as "session fully initialized for work".
@@ -267,13 +528,30 @@ export function runSessionStart(projectRoot, options = {}) {
267
528
  if (posture === READ_ONLY_POSTURE) {
268
529
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
269
530
  }
531
+ // #2992: re-arm path refreshes clock/bind without fat cold ceremony.
532
+ if (ceremonyTier === REARM_CEREMONY_TIER) {
533
+ return runSessionRearm(projectRoot, options, instant, environment);
534
+ }
535
+ const overallStarted = performance.now();
536
+ const stepTimings = [];
537
+ const allowOptionalNetwork = resolveSessionStartOptionalNetwork(options);
270
538
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
271
539
  if (gitHeadValue === null) {
272
540
  const payload = {
273
541
  ready: false,
542
+ exit_code: 2,
543
+ ceremony_tier: COLD_CEREMONY_TIER,
274
544
  environment: environmentContextToDict(environment),
275
545
  message: gitError ?? "could not resolve git HEAD",
276
546
  };
547
+ // #2994: still record failed attempt duration for process-cost rollups.
548
+ emitSessionStartProcessCost({
549
+ ceremonyTier: COLD_CEREMONY_TIER,
550
+ durationMs: elapsedMs(overallStarted),
551
+ exitCode: 2,
552
+ ready: false,
553
+ optionalNetwork: allowOptionalNetwork,
554
+ }, { projectRoot });
277
555
  return {
278
556
  code: 2,
279
557
  payload,
@@ -290,6 +568,7 @@ export function runSessionStart(projectRoot, options = {}) {
290
568
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
291
569
  const userMd = resolveUserMd(projectRoot);
292
570
  if (!quickSteps.alignment) {
571
+ const stepStarted = performance.now();
293
572
  const message = "Deft Directive active -- AGENTS.md loaded.";
294
573
  // Sanitize newlines on the data-derived path/diagnostic before they land in
295
574
  // the ritual-step message / terminal output (matches doctor's CWE-116
@@ -300,25 +579,26 @@ export function runSessionStart(projectRoot, options = {}) {
300
579
  const userMdLine = userMd.found
301
580
  ? `USER.md resolved (${userMd.rung}): ${safePath}`
302
581
  : safeDiagnostic;
582
+ const durationMs = elapsedMs(stepStarted);
303
583
  quickSteps.alignment = ritualStep({
304
584
  ok: true,
305
585
  ts: instant,
306
586
  message: `${message} ${userMdLine}`,
587
+ durationMs,
307
588
  });
589
+ stepTimings.push({ name: "alignment", duration_ms: durationMs });
308
590
  lines.push(message);
309
591
  lines.push(userMdLine);
310
592
  }
593
+ else {
594
+ stepTimings.push({ name: "alignment", duration_ms: 0, skipped: true });
595
+ }
311
596
  lines.push(formatEnvironmentContext(environment));
312
597
  if (!quickSteps.branch_policy) {
598
+ const stepStarted = performance.now();
313
599
  const result = resolvePolicy(projectRoot);
314
600
  const message = disclosureLine(result);
315
601
  const ok = result.error === null || result.source === "default-fail-closed";
316
- quickSteps.branch_policy = ritualStep({
317
- ok,
318
- ts: instant,
319
- message,
320
- exitCode: ok ? 0 : 2,
321
- });
322
602
  lines.push(message);
323
603
  // Human merge gate disclosure (#1193) — surface when ON (or env bypass active).
324
604
  const humanMerge = resolveHumanMergePolicy(projectRoot);
@@ -330,18 +610,35 @@ export function runSessionStart(projectRoot, options = {}) {
330
610
  if (branchSync.warning) {
331
611
  lines.push(branchSync.warning);
332
612
  }
333
- }
334
- const verifyToolsFn = options.verifyTools ??
335
- ((output) => {
336
- const toolLines = [];
337
- const result = verifyRequiredTools({ outputFn: (line) => toolLines.push(line) });
338
- for (const line of toolLines) {
339
- output(line);
340
- }
341
- return { exitCode: result.exitCode };
613
+ const durationMs = elapsedMs(stepStarted);
614
+ quickSteps.branch_policy = ritualStep({
615
+ ok,
616
+ ts: instant,
617
+ message,
618
+ exitCode: ok ? 0 : 2,
619
+ durationMs,
342
620
  });
343
- verifyToolsFn((line) => lines.push(line));
621
+ stepTimings.push({ name: "branch_policy", duration_ms: durationMs });
622
+ }
623
+ else {
624
+ stepTimings.push({ name: "branch_policy", duration_ms: 0, skipped: true });
625
+ }
626
+ {
627
+ const stepStarted = performance.now();
628
+ const verifyToolsFn = options.verifyTools ??
629
+ ((output) => {
630
+ const toolLines = [];
631
+ const result = verifyRequiredTools({ outputFn: (line) => toolLines.push(line) });
632
+ for (const line of toolLines) {
633
+ output(line);
634
+ }
635
+ return { exitCode: result.exitCode };
636
+ });
637
+ verifyToolsFn((line) => lines.push(line));
638
+ stepTimings.push({ name: "verify_tools", duration_ms: elapsedMs(stepStarted) });
639
+ }
344
640
  if (!quickSteps.triage_welcome) {
641
+ const stepStarted = performance.now();
345
642
  const captured = [];
346
643
  const triageCommand = ["triage_welcome.run_default_mode", "--project-root", projectRoot];
347
644
  try {
@@ -350,6 +647,13 @@ export function runSessionStart(projectRoot, options = {}) {
350
647
  const outcome = runDefaultMode(root, {
351
648
  output: welcomeOpts.output,
352
649
  writeHistory: welcomeOpts.writeHistory,
650
+ now: welcomeOpts.now,
651
+ // #2991: default hot path skips ensureTriageCacheHydrated / maybeSelfHealCache.
652
+ selfHealFn: allowOptionalNetwork
653
+ ? undefined
654
+ : () => {
655
+ /* no network cache work on hot path */
656
+ },
353
657
  });
354
658
  return { exitCode: outcome.exitCode };
355
659
  });
@@ -360,33 +664,51 @@ export function runSessionStart(projectRoot, options = {}) {
360
664
  });
361
665
  const ok = outcome.exitCode === 0;
362
666
  const message = captured.join("\n").trim() || "triage welcome completed";
667
+ const durationMs = elapsedMs(stepStarted);
363
668
  quickSteps.triage_welcome = ritualStep({
364
669
  ok,
365
670
  ts: instant,
366
671
  message,
367
672
  exitCode: outcome.exitCode,
368
673
  command: triageCommand,
674
+ durationMs,
369
675
  });
676
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
370
677
  lines.push(...captured);
371
678
  }
372
679
  catch (exc) {
373
680
  const message = `triage welcome failed: ${String(exc)}`;
681
+ const durationMs = elapsedMs(stepStarted);
374
682
  quickSteps.triage_welcome = ritualStep({
375
683
  ok: false,
376
684
  ts: instant,
377
685
  message,
378
686
  exitCode: 2,
379
687
  command: triageCommand,
688
+ durationMs,
380
689
  });
690
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
381
691
  lines.push(message);
382
692
  }
383
693
  }
384
- try {
385
- const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
386
- lines.push(...releaseAvailability.lines);
694
+ else {
695
+ stepTimings.push({ name: "triage_welcome", duration_ms: 0, skipped: true });
387
696
  }
388
- catch {
389
- // Release availability is a best-effort operator advisory, never a session blocker.
697
+ // #2991: npm release probe is optional network — off by default so ritual write is not blocked.
698
+ if (allowOptionalNetwork) {
699
+ const stepStarted = performance.now();
700
+ try {
701
+ const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
702
+ lines.push(...releaseAvailability.lines);
703
+ }
704
+ catch {
705
+ // Release availability is a best-effort operator advisory, never a session blocker.
706
+ }
707
+ stepTimings.push({ name: "release_probe", duration_ms: elapsedMs(stepStarted) });
708
+ }
709
+ else {
710
+ stepTimings.push({ name: "release_probe", duration_ms: 0, skipped: true });
711
+ lines.push(OPTIONAL_NETWORK_SKIPPED_MESSAGE);
390
712
  }
391
713
  try {
392
714
  const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
@@ -422,6 +744,7 @@ export function runSessionStart(projectRoot, options = {}) {
422
744
  if (consentPrompt.length > 0) {
423
745
  lines.push(consentPrompt.trimEnd());
424
746
  }
747
+ const writeStarted = performance.now();
425
748
  const payload = newRitualStatePayload({
426
749
  sessionId: (options.newSessionId ?? randomUUID)(),
427
750
  gitHead: gitHeadValue,
@@ -430,17 +753,39 @@ export function runSessionStart(projectRoot, options = {}) {
430
753
  quickSteps,
431
754
  gatedSteps,
432
755
  });
433
- const statePath = writeRitualState(projectRoot, payload);
756
+ let statePath;
757
+ try {
758
+ statePath = writeRitualState(projectRoot, payload);
759
+ }
760
+ catch (cause) {
761
+ // #2994: still record failed attempt when ritual-state write throws.
762
+ stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
763
+ emitSessionStartProcessCost({
764
+ ceremonyTier: COLD_CEREMONY_TIER,
765
+ durationMs: elapsedMs(overallStarted),
766
+ exitCode: 2,
767
+ ready: false,
768
+ optionalNetwork: allowOptionalNetwork,
769
+ steps: stepTimings,
770
+ }, { projectRoot });
771
+ throw cause;
772
+ }
773
+ stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
434
774
  const failed = Object.entries(quickSteps)
435
775
  .filter(([, step]) => !step.ok && !step.deferred_reason)
436
776
  .map(([name]) => name);
437
777
  const code = failed.length > 0 ? 1 : 0;
778
+ const totalMs = elapsedMs(overallStarted);
438
779
  const resultPayload = {
439
780
  ready: code === 0,
440
781
  exit_code: code,
782
+ ceremony_tier: COLD_CEREMONY_TIER,
441
783
  state_path: statePath,
442
784
  quick_steps: quickSteps,
443
785
  gated_steps: gatedSteps,
786
+ steps: stepTimings,
787
+ duration_ms: totalMs,
788
+ optional_network: allowOptionalNetwork,
444
789
  user_md: {
445
790
  path: userMd.path,
446
791
  rung: userMd.rung,
@@ -450,6 +795,15 @@ export function runSessionStart(projectRoot, options = {}) {
450
795
  environment: environmentContextToDict(environment),
451
796
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
452
797
  };
798
+ // #2994: local process-cost event (best-effort; never blocks ceremony).
799
+ emitSessionStartProcessCost({
800
+ ceremonyTier: COLD_CEREMONY_TIER,
801
+ durationMs: totalMs,
802
+ exitCode: code,
803
+ ready: code === 0,
804
+ optionalNetwork: allowOptionalNetwork,
805
+ steps: stepTimings,
806
+ }, { projectRoot });
453
807
  return { code, payload: resultPayload, lines };
454
808
  }
455
809
  export { ritualStatePath };
@@ -1,5 +1,6 @@
1
1
  import { type GitRunner } from "./git.js";
2
2
  import { type DirectivePosture } from "./posture.js";
3
+ import { type SessionCeremonyTier } from "./session-start.js";
3
4
  export { formatCacheFetchAllRecoveryCommand, recoveryHintForStaleFailure, } from "./cache-recovery.js";
4
5
  export declare const ENV_SKIP = "DEFT_SESSION_RITUAL_SKIP";
5
6
  export { ENTRYPOINT_TIMEOUT_EXIT_CODE, ENTRYPOINT_TIMEOUT_SECONDS, } from "./ritual-entrypoint.js";
@@ -13,12 +14,19 @@ export interface VerifyResult {
13
14
  readonly wouldFailCode: number | null;
14
15
  readonly posture: DirectivePosture;
15
16
  readonly ritualStateRequired: boolean;
17
+ /**
18
+ * Preferred session:start recovery when code !== 0 (#2992).
19
+ * `rearm` when age/compact stale on an otherwise valid bind; `cold` otherwise.
20
+ */
21
+ readonly recoveryTier?: SessionCeremonyTier | null;
16
22
  }
17
23
  export type RitualRunner = (command: readonly string[], projectRoot: string) => {
18
24
  code: number;
19
25
  stdout: string;
20
26
  stderr: string;
21
27
  };
28
+ /** Human recovery line after a failed ritual probe (#2992 / #2993). */
29
+ export declare function formatRitualRecoveryInstruction(tier?: SessionCeremonyTier): string;
22
30
  export interface VerifySessionRitualOptions {
23
31
  readonly tier?: "quick" | "gated";
24
32
  readonly now?: Date;