@deftai/directive-core 0.89.0 → 0.91.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 (100) 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 +143 -0
  8. package/dist/doctor/openclaw-skills.js +706 -0
  9. package/dist/doctor/types.d.ts +11 -0
  10. package/dist/eval/health.d.ts +10 -1
  11. package/dist/eval/health.js +16 -16
  12. package/dist/hooks/dispatcher.js +20 -5
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/init-deposit/hygiene.js +3 -0
  16. package/dist/init-deposit/init-deposit.d.ts +5 -0
  17. package/dist/init-deposit/init-deposit.js +37 -2
  18. package/dist/intake/github-auth-modes.d.ts +1 -1
  19. package/dist/intake/github-auth-modes.js +25 -3
  20. package/dist/intake/issue-ingest-cli.js +13 -0
  21. package/dist/intake/reconcile-issues-cli.js +13 -0
  22. package/dist/lifecycle/events.js +2 -0
  23. package/dist/lifecycle/index.d.ts +1 -0
  24. package/dist/lifecycle/index.js +1 -0
  25. package/dist/lifecycle/stats.d.ts +53 -0
  26. package/dist/lifecycle/stats.js +286 -0
  27. package/dist/policy/plan-extensions.d.ts +10 -0
  28. package/dist/policy/plan-extensions.js +16 -0
  29. package/dist/pr-closing-keywords/detect.d.ts +13 -0
  30. package/dist/pr-closing-keywords/detect.js +73 -8
  31. package/dist/pr-closing-keywords/index.d.ts +2 -2
  32. package/dist/pr-closing-keywords/index.js +1 -1
  33. package/dist/pr-closing-keywords/main.js +119 -90
  34. package/dist/pr-closing-keywords/types.d.ts +10 -0
  35. package/dist/release/spawn.js +13 -0
  36. package/dist/release-e2e/git-ops.d.ts +7 -0
  37. package/dist/release-e2e/git-ops.js +35 -2
  38. package/dist/render/constants.d.ts +16 -0
  39. package/dist/render/constants.js +16 -1
  40. package/dist/render/export-spec.d.ts +7 -1
  41. package/dist/render/export-spec.js +51 -5
  42. package/dist/render/index.d.ts +3 -2
  43. package/dist/render/index.js +2 -2
  44. package/dist/render/roadmap-render.js +164 -60
  45. package/dist/render/scope-outlook.d.ts +5 -0
  46. package/dist/render/scope-outlook.js +3 -0
  47. package/dist/render/spec-render.d.ts +28 -2
  48. package/dist/render/spec-render.js +111 -14
  49. package/dist/render/text-utils.d.ts +6 -0
  50. package/dist/render/text-utils.js +23 -0
  51. package/dist/scm/binary.d.ts +3 -0
  52. package/dist/scm/binary.js +10 -2
  53. package/dist/scm/call.d.ts +5 -0
  54. package/dist/scm/call.js +5 -0
  55. package/dist/scm/index.d.ts +1 -0
  56. package/dist/scm/index.js +1 -0
  57. package/dist/scm/main.d.ts +5 -0
  58. package/dist/scm/main.js +27 -0
  59. package/dist/scm/readiness-cli.d.ts +24 -0
  60. package/dist/scm/readiness-cli.js +85 -0
  61. package/dist/scm/readiness.d.ts +104 -0
  62. package/dist/scm/readiness.js +421 -0
  63. package/dist/scope/batch-promote.d.ts +26 -0
  64. package/dist/scope/batch-promote.js +167 -0
  65. package/dist/scope/index.d.ts +1 -0
  66. package/dist/scope/index.js +1 -0
  67. package/dist/scope/main.d.ts +2 -0
  68. package/dist/scope/main.js +58 -7
  69. package/dist/scope/wip-cap-check.d.ts +6 -0
  70. package/dist/scope/wip-cap-check.js +14 -2
  71. package/dist/session/index.d.ts +2 -0
  72. package/dist/session/index.js +2 -0
  73. package/dist/session/process-cost-constants.d.ts +9 -0
  74. package/dist/session/process-cost-constants.js +11 -0
  75. package/dist/session/process-cost.d.ts +53 -0
  76. package/dist/session/process-cost.js +82 -0
  77. package/dist/session/ritual-sentinel.d.ts +5 -0
  78. package/dist/session/ritual-sentinel.js +12 -1
  79. package/dist/session/session-ready.d.ts +67 -0
  80. package/dist/session/session-ready.js +264 -0
  81. package/dist/session/session-start.d.ts +63 -1
  82. package/dist/session/session-start.js +432 -24
  83. package/dist/session/verify-session-ritual.d.ts +8 -0
  84. package/dist/session/verify-session-ritual.js +95 -35
  85. package/dist/tool-events/classify.d.ts +20 -0
  86. package/dist/tool-events/classify.js +634 -0
  87. package/dist/tool-events/index.d.ts +10 -0
  88. package/dist/tool-events/index.js +10 -0
  89. package/dist/tool-events/summarize.d.ts +34 -0
  90. package/dist/tool-events/summarize.js +93 -0
  91. package/dist/tool-events/types.d.ts +52 -0
  92. package/dist/tool-events/types.js +13 -0
  93. package/dist/triage/welcome/index.d.ts +1 -1
  94. package/dist/triage/welcome/index.js +1 -1
  95. package/dist/triage/welcome/onboard.js +7 -1
  96. package/dist/triage/welcome/prior-state.d.ts +7 -0
  97. package/dist/triage/welcome/prior-state.js +16 -2
  98. package/dist/triage/welcome/writers.d.ts +10 -0
  99. package/dist/triage/welcome/writers.js +72 -2
  100. package/package.json +7 -3
@@ -8,21 +8,38 @@ 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";
12
+ import { formatScmReadinessLines, probeScmReadiness, scmReadinessToDict, } from "../scm/readiness.js";
11
13
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
12
14
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
13
15
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
14
16
  import { emitSessionValueReadback } from "../value/readback.js";
15
17
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
16
- import { defaultGitRunner, gitHead, worktreePath } from "./git.js";
18
+ import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
19
+ import { emitSessionStartProcessCost } from "./process-cost.js";
17
20
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
18
- import { newRitualStatePayload, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
21
+ import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
19
22
  export const SESSION_POSTURES = ["read-only", "mutation"];
20
23
  export const READ_ONLY_POSTURE = "read-only";
21
24
  export const MUTATION_POSTURE = "mutation";
22
25
  export const READ_ONLY_ALIGNMENT_MESSAGE = "Deft Directive active -- AGENTS.md loaded.";
23
26
  export const READ_ONLY_RESULT_MESSAGE = "read-only session posture (alignment only; no ritual-state write)";
27
+ /** Cold (full) vs re-arm (clock/HEAD refresh) ceremony tiers (#2992). */
28
+ export const SESSION_CEREMONY_TIERS = ["cold", "rearm"];
29
+ export const COLD_CEREMONY_TIER = "cold";
30
+ export const REARM_CEREMONY_TIER = "rearm";
24
31
  export const QUICK_STEPS = ["alignment", "branch_policy", "triage_welcome"];
25
32
  export const GATED_STEPS = ["doctor", "cache_fresh"];
33
+ /** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
34
+ export const ENV_SESSION_START_NETWORK = "DEFT_SESSION_START_NETWORK";
35
+ /** Human-readable skip notice when optional network is off the hot path (#2991). */
36
+ export const OPTIONAL_NETWORK_SKIPPED_MESSAGE = "[deft session] optional network skipped (release probe, triage cache hydrate/self-heal); " +
37
+ "pass --with-network or set DEFT_SESSION_START_NETWORK=1 to enable.";
38
+ /** Re-arm skips fat cold-path work when prior ritual is still structurally valid (#2992). */
39
+ export const REARM_SKIPPED_FAT_PATH_MESSAGE = "[deft session] re-arm tier: refreshed ritual clock + HEAD/worktree bind; " +
40
+ "skipped verify:tools, triage welcome, release probe, and staleness tickler.";
41
+ /** When --rearm is requested but state requires a full cold ceremony (#2992). */
42
+ export const REARM_INELIGIBLE_PREFIX = "session re-arm refused — full cold session:start required:";
26
43
  const STEP_ALIASES = {
27
44
  branch: "branch_policy",
28
45
  "branch-policy": "branch_policy",
@@ -31,6 +48,89 @@ const STEP_ALIASES = {
31
48
  triage: "triage_welcome",
32
49
  "triage-welcome": "triage_welcome",
33
50
  };
51
+ /** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
52
+ export function formatSessionStartRecoveryCommand(tier = "cold") {
53
+ if (tier === "rearm") {
54
+ return formatFrameworkCommand(["session:start", "--rearm"]);
55
+ }
56
+ return formatFrameworkCommand(["session:start"]);
57
+ }
58
+ function stepPassesForRearm(step) {
59
+ if (!step || typeof step !== "object")
60
+ return false;
61
+ if (step.deferred_reason)
62
+ return true;
63
+ return step.ok === true;
64
+ }
65
+ /**
66
+ * Whether a prior ritual can be re-armed without a full cold ceremony (#2992).
67
+ * Requires valid state, same worktree, continuous (or identical) HEAD, and
68
+ * previously-passing quick steps.
69
+ */
70
+ export function assessRearmEligibility(projectRoot, options = {}) {
71
+ const runGit = options.runGit ?? defaultGitRunner;
72
+ const [state, err] = readRitualState(projectRoot);
73
+ if (state === null) {
74
+ return {
75
+ eligible: false,
76
+ reason: err ?? "ritual state missing (first install / cold path required)",
77
+ };
78
+ }
79
+ const { head: currentHead, error: headError } = gitHead(projectRoot, runGit);
80
+ if (currentHead === null) {
81
+ return { eligible: false, reason: headError ?? "could not resolve git HEAD" };
82
+ }
83
+ const currentWorktree = worktreePath(projectRoot, runGit);
84
+ if (state.worktreePath !== currentWorktree) {
85
+ return {
86
+ eligible: false,
87
+ reason: `ritual state belongs to a different worktree (${state.worktreePath})`,
88
+ };
89
+ }
90
+ if (state.gitHead !== currentHead) {
91
+ const forward = gitIsAncestor(projectRoot, state.gitHead, currentHead, runGit);
92
+ if (forward === null) {
93
+ return { eligible: false, reason: "could not verify git history for session re-arm" };
94
+ }
95
+ if (!forward) {
96
+ return {
97
+ eligible: false,
98
+ reason: "git HEAD changed discontinuously (full cold session:start required)",
99
+ };
100
+ }
101
+ }
102
+ for (const stepName of QUICK_STEPS) {
103
+ if (!stepPassesForRearm(state.quickSteps[stepName])) {
104
+ return {
105
+ eligible: false,
106
+ reason: `quick step '${stepName}' is missing or failed (full cold session:start required)`,
107
+ };
108
+ }
109
+ }
110
+ return { eligible: true, state, currentHead, currentWorktree };
111
+ }
112
+ function restampSteps(steps, now) {
113
+ const out = {};
114
+ for (const [name, step] of Object.entries(steps)) {
115
+ out[name] = {
116
+ ...step,
117
+ ts: ritualStep({ ok: step.ok === true, ts: now }).ts,
118
+ };
119
+ }
120
+ return out;
121
+ }
122
+ /** Resolve whether optional session:start network work is enabled (#2991). */
123
+ export function resolveSessionStartOptionalNetwork(options = {}) {
124
+ if (options.allowOptionalNetwork === true)
125
+ return true;
126
+ if (options.allowOptionalNetwork === false)
127
+ return false;
128
+ const env = options.env ?? process.env;
129
+ return env[ENV_SESSION_START_NETWORK] === "1";
130
+ }
131
+ function elapsedMs(started) {
132
+ return Math.max(0, Math.round(performance.now() - started));
133
+ }
34
134
  function normaliseStepName(name) {
35
135
  return STEP_ALIASES[name] ?? name;
36
136
  }
@@ -193,6 +293,44 @@ export function defaultBranchSync(projectRoot, runGit = defaultGitRunner) {
193
293
  }
194
294
  return { branch, upstream, ahead, behind, warning };
195
295
  }
296
+ /**
297
+ * Resolve SCM readiness for session orientation (#2275).
298
+ * Shallow by default (PATH + auth status); deep when optional network is on.
299
+ * Never throws — session:start must not hard-block on SCM absence.
300
+ */
301
+ function resolveSessionScmReadiness(options, allowOptionalNetwork) {
302
+ const probe = options.probeScm ?? probeScmReadiness;
303
+ try {
304
+ return probe({
305
+ depth: allowOptionalNetwork ? "deep" : "shallow",
306
+ env: options.env,
307
+ });
308
+ }
309
+ catch {
310
+ return {
311
+ ready: false,
312
+ binary: null,
313
+ binaryPath: null,
314
+ authState: "unknown",
315
+ githubAuthMode: "host-gh",
316
+ runtimeMode: "local-unsandboxed",
317
+ injectedTokenPresent: false,
318
+ depth: allowOptionalNetwork ? "deep" : "shallow",
319
+ detail: "SCM readiness probe failed unexpectedly; treating SCM-dependent gates as skipped",
320
+ remediation: null,
321
+ skippedGates: [
322
+ "triage:queue",
323
+ "issue:ingest",
324
+ "pr:*",
325
+ "reconcile:issues",
326
+ "cache:fetch-all",
327
+ "scm:*",
328
+ ],
329
+ login: null,
330
+ failureKind: "probe_error",
331
+ };
332
+ }
333
+ }
196
334
  function runReadOnlySessionStart(projectRoot, options, instant, environment) {
197
335
  const lines = [];
198
336
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
@@ -202,9 +340,12 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
202
340
  const userMdLine = userMd.found
203
341
  ? `USER.md resolved (${userMd.rung}): ${safePath}`
204
342
  : safeDiagnostic;
343
+ // #2275: report SCM availability even on read-only alignment (shallow; no network).
344
+ const scm = resolveSessionScmReadiness(options, false);
205
345
  lines.push(READ_ONLY_ALIGNMENT_MESSAGE);
206
346
  lines.push(userMdLine);
207
347
  lines.push(formatEnvironmentContext(environment));
348
+ lines.push(...formatScmReadinessLines(scm));
208
349
  const resultPayload = {
209
350
  ready: true,
210
351
  exit_code: 0,
@@ -225,16 +366,183 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
225
366
  diagnostic: userMd.diagnostic,
226
367
  },
227
368
  environment: environmentContextToDict(environment),
369
+ scm: scmReadinessToDict(scm),
228
370
  message: READ_ONLY_RESULT_MESSAGE,
229
371
  };
230
372
  return { code: 0, payload: resultPayload, lines };
231
373
  }
374
+ function runSessionRearm(projectRoot, options, instant, environment) {
375
+ const overallStarted = performance.now();
376
+ const runGit = options.runGit ?? defaultGitRunner;
377
+ const eligibility = assessRearmEligibility(projectRoot, { runGit });
378
+ if (!eligibility.eligible) {
379
+ const coldCmd = formatSessionStartRecoveryCommand("cold");
380
+ const message = `${REARM_INELIGIBLE_PREFIX} ${eligibility.reason}. Run \`${coldCmd}\`.`;
381
+ // #2994: still record failed attempt duration for process-cost rollups.
382
+ emitSessionStartProcessCost({
383
+ ceremonyTier: REARM_CEREMONY_TIER,
384
+ durationMs: elapsedMs(overallStarted),
385
+ exitCode: 1,
386
+ ready: false,
387
+ optionalNetwork: false,
388
+ }, { projectRoot });
389
+ return {
390
+ code: 1,
391
+ payload: {
392
+ ready: false,
393
+ exit_code: 1,
394
+ ceremony_tier: REARM_CEREMONY_TIER,
395
+ rearm_eligible: false,
396
+ environment: environmentContextToDict(environment),
397
+ message,
398
+ },
399
+ lines: [formatEnvironmentContext(environment), message],
400
+ };
401
+ }
402
+ const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
403
+ const userMd = resolveUserMd(projectRoot);
404
+ const safePath = userMd.path.replace(/\r?\n/g, " ");
405
+ const safeDiagnostic = userMd.diagnostic.replace(/\r?\n/g, " ");
406
+ const userMdLine = userMd.found
407
+ ? `USER.md resolved (${userMd.rung}): ${safePath}`
408
+ : safeDiagnostic;
409
+ const alignmentMessage = `${READ_ONLY_ALIGNMENT_MESSAGE} ${userMdLine}`;
410
+ // #2275: re-arm still reports SCM state (shallow; no network).
411
+ const scm = resolveSessionScmReadiness(options, false);
412
+ const lines = [
413
+ READ_ONLY_ALIGNMENT_MESSAGE,
414
+ userMdLine,
415
+ formatEnvironmentContext(environment),
416
+ ...formatScmReadinessLines(scm),
417
+ REARM_SKIPPED_FAT_PATH_MESSAGE,
418
+ ];
419
+ // Light branch-policy disclosure (local only) so re-arm still surfaces policy state.
420
+ const policyResult = resolvePolicy(projectRoot);
421
+ const policyMessage = disclosureLine(policyResult);
422
+ lines.push(policyMessage);
423
+ const humanMerge = resolveHumanMergePolicy(projectRoot);
424
+ const humanMergeLine = humanMergeDisclosureLine(humanMerge);
425
+ if (humanMergeLine !== null) {
426
+ lines.push(humanMergeLine);
427
+ }
428
+ const priorQuick = eligibility.state.quickSteps;
429
+ const priorTriage = priorQuick.triage_welcome ?? ritualStep({ ok: true, ts: instant });
430
+ const policyOk = policyResult.error === null || policyResult.source === "default-fail-closed";
431
+ const quickSteps = {
432
+ alignment: ritualStep({
433
+ ok: true,
434
+ ts: instant,
435
+ message: alignmentMessage,
436
+ durationMs: 0,
437
+ }),
438
+ branch_policy: ritualStep({
439
+ ok: policyOk,
440
+ ts: instant,
441
+ message: policyMessage,
442
+ exitCode: policyOk ? 0 : 2,
443
+ durationMs: 0,
444
+ }),
445
+ // Preserve prior triage outcome; do not re-run welcome / self-heal on re-arm.
446
+ triage_welcome: {
447
+ ...priorTriage,
448
+ ts: ritualStep({ ok: priorTriage.ok === true, ts: instant }).ts,
449
+ message: typeof priorTriage.message === "string"
450
+ ? priorTriage.message
451
+ : "triage welcome preserved on re-arm",
452
+ },
453
+ };
454
+ const gatedSteps = restampSteps(eligibility.state.gatedSteps, instant);
455
+ const writeStarted = performance.now();
456
+ // Fresh payload (no rearm_needed / compact_resume_at) clears compact markers (#2992).
457
+ const writePayload = {
458
+ ...newRitualStatePayload({
459
+ sessionId: (options.newSessionId ?? randomUUID)(),
460
+ gitHead: eligibility.currentHead,
461
+ worktreePath: eligibility.currentWorktree,
462
+ startedAt: instant,
463
+ quickSteps,
464
+ gatedSteps,
465
+ }),
466
+ ceremony_tier: REARM_CEREMONY_TIER,
467
+ };
468
+ let statePath;
469
+ try {
470
+ statePath = writeRitualState(projectRoot, writePayload);
471
+ }
472
+ catch (cause) {
473
+ // #2994: still record failed attempt when ritual-state write throws.
474
+ emitSessionStartProcessCost({
475
+ ceremonyTier: REARM_CEREMONY_TIER,
476
+ durationMs: elapsedMs(overallStarted),
477
+ exitCode: 2,
478
+ ready: false,
479
+ optionalNetwork: false,
480
+ steps: [
481
+ { name: "alignment", duration_ms: 0 },
482
+ { name: "branch_policy", duration_ms: 0 },
483
+ { name: "verify_tools", duration_ms: 0, skipped: true },
484
+ { name: "triage_welcome", duration_ms: 0, skipped: true },
485
+ { name: "release_probe", duration_ms: 0, skipped: true },
486
+ { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
487
+ ],
488
+ }, { projectRoot });
489
+ throw cause;
490
+ }
491
+ const stepTimings = [
492
+ { name: "alignment", duration_ms: 0 },
493
+ { name: "branch_policy", duration_ms: 0 },
494
+ { name: "verify_tools", duration_ms: 0, skipped: true },
495
+ { name: "triage_welcome", duration_ms: 0, skipped: true },
496
+ { name: "release_probe", duration_ms: 0, skipped: true },
497
+ { name: "ritual_write", duration_ms: elapsedMs(writeStarted) },
498
+ ];
499
+ const totalMs = elapsedMs(overallStarted);
500
+ const failed = Object.entries(quickSteps)
501
+ .filter(([, step]) => !step.ok && !step.deferred_reason)
502
+ .map(([name]) => name);
503
+ const code = failed.length > 0 ? 1 : 0;
504
+ // #2994: local process-cost event (best-effort; never blocks ceremony).
505
+ emitSessionStartProcessCost({
506
+ ceremonyTier: REARM_CEREMONY_TIER,
507
+ durationMs: totalMs,
508
+ exitCode: code,
509
+ ready: code === 0,
510
+ optionalNetwork: false,
511
+ steps: stepTimings,
512
+ }, { projectRoot });
513
+ return {
514
+ code,
515
+ payload: {
516
+ ready: code === 0,
517
+ exit_code: code,
518
+ ceremony_tier: REARM_CEREMONY_TIER,
519
+ rearm_eligible: true,
520
+ state_path: statePath,
521
+ quick_steps: quickSteps,
522
+ gated_steps: gatedSteps,
523
+ steps: stepTimings,
524
+ duration_ms: totalMs,
525
+ optional_network: false,
526
+ user_md: {
527
+ path: userMd.path,
528
+ rung: userMd.rung,
529
+ found: userMd.found,
530
+ diagnostic: userMd.diagnostic,
531
+ },
532
+ environment: environmentContextToDict(environment),
533
+ scm: scmReadinessToDict(scm),
534
+ message: code === 0 ? "session ritual re-armed" : "session ritual re-arm failed",
535
+ },
536
+ lines,
537
+ };
538
+ }
232
539
  export function runSessionStart(projectRoot, options = {}) {
233
540
  const posture = options.posture ?? MUTATION_POSTURE;
234
541
  const instant = options.now ?? new Date();
235
542
  const deferrals = options.deferrals ?? {};
236
543
  const runGit = options.runGit ?? defaultGitRunner;
237
544
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
545
+ const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
238
546
  // #2926: official root opt-out wins locally — skip Directive session ritual.
239
547
  // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
240
548
  // automation does not treat opt-out as "session fully initialized for work".
@@ -267,13 +575,30 @@ export function runSessionStart(projectRoot, options = {}) {
267
575
  if (posture === READ_ONLY_POSTURE) {
268
576
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
269
577
  }
578
+ // #2992: re-arm path refreshes clock/bind without fat cold ceremony.
579
+ if (ceremonyTier === REARM_CEREMONY_TIER) {
580
+ return runSessionRearm(projectRoot, options, instant, environment);
581
+ }
582
+ const overallStarted = performance.now();
583
+ const stepTimings = [];
584
+ const allowOptionalNetwork = resolveSessionStartOptionalNetwork(options);
270
585
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
271
586
  if (gitHeadValue === null) {
272
587
  const payload = {
273
588
  ready: false,
589
+ exit_code: 2,
590
+ ceremony_tier: COLD_CEREMONY_TIER,
274
591
  environment: environmentContextToDict(environment),
275
592
  message: gitError ?? "could not resolve git HEAD",
276
593
  };
594
+ // #2994: still record failed attempt duration for process-cost rollups.
595
+ emitSessionStartProcessCost({
596
+ ceremonyTier: COLD_CEREMONY_TIER,
597
+ durationMs: elapsedMs(overallStarted),
598
+ exitCode: 2,
599
+ ready: false,
600
+ optionalNetwork: allowOptionalNetwork,
601
+ }, { projectRoot });
277
602
  return {
278
603
  code: 2,
279
604
  payload,
@@ -290,6 +615,7 @@ export function runSessionStart(projectRoot, options = {}) {
290
615
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
291
616
  const userMd = resolveUserMd(projectRoot);
292
617
  if (!quickSteps.alignment) {
618
+ const stepStarted = performance.now();
293
619
  const message = "Deft Directive active -- AGENTS.md loaded.";
294
620
  // Sanitize newlines on the data-derived path/diagnostic before they land in
295
621
  // the ritual-step message / terminal output (matches doctor's CWE-116
@@ -300,25 +626,32 @@ export function runSessionStart(projectRoot, options = {}) {
300
626
  const userMdLine = userMd.found
301
627
  ? `USER.md resolved (${userMd.rung}): ${safePath}`
302
628
  : safeDiagnostic;
629
+ const durationMs = elapsedMs(stepStarted);
303
630
  quickSteps.alignment = ritualStep({
304
631
  ok: true,
305
632
  ts: instant,
306
633
  message: `${message} ${userMdLine}`,
634
+ durationMs,
307
635
  });
636
+ stepTimings.push({ name: "alignment", duration_ms: durationMs });
308
637
  lines.push(message);
309
638
  lines.push(userMdLine);
310
639
  }
640
+ else {
641
+ stepTimings.push({ name: "alignment", duration_ms: 0, skipped: true });
642
+ }
311
643
  lines.push(formatEnvironmentContext(environment));
644
+ // #2275: SCM tooling + auth readiness — shallow on hot path, deep with --with-network.
645
+ // Never fails session:start; reports which SCM-dependent gates are skipped.
646
+ const scmStepStarted = performance.now();
647
+ const scm = resolveSessionScmReadiness(options, allowOptionalNetwork);
648
+ lines.push(...formatScmReadinessLines(scm));
649
+ stepTimings.push({ name: "scm_readiness", duration_ms: elapsedMs(scmStepStarted) });
312
650
  if (!quickSteps.branch_policy) {
651
+ const stepStarted = performance.now();
313
652
  const result = resolvePolicy(projectRoot);
314
653
  const message = disclosureLine(result);
315
654
  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
655
  lines.push(message);
323
656
  // Human merge gate disclosure (#1193) — surface when ON (or env bypass active).
324
657
  const humanMerge = resolveHumanMergePolicy(projectRoot);
@@ -330,18 +663,35 @@ export function runSessionStart(projectRoot, options = {}) {
330
663
  if (branchSync.warning) {
331
664
  lines.push(branchSync.warning);
332
665
  }
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 };
666
+ const durationMs = elapsedMs(stepStarted);
667
+ quickSteps.branch_policy = ritualStep({
668
+ ok,
669
+ ts: instant,
670
+ message,
671
+ exitCode: ok ? 0 : 2,
672
+ durationMs,
342
673
  });
343
- verifyToolsFn((line) => lines.push(line));
674
+ stepTimings.push({ name: "branch_policy", duration_ms: durationMs });
675
+ }
676
+ else {
677
+ stepTimings.push({ name: "branch_policy", duration_ms: 0, skipped: true });
678
+ }
679
+ {
680
+ const stepStarted = performance.now();
681
+ const verifyToolsFn = options.verifyTools ??
682
+ ((output) => {
683
+ const toolLines = [];
684
+ const result = verifyRequiredTools({ outputFn: (line) => toolLines.push(line) });
685
+ for (const line of toolLines) {
686
+ output(line);
687
+ }
688
+ return { exitCode: result.exitCode };
689
+ });
690
+ verifyToolsFn((line) => lines.push(line));
691
+ stepTimings.push({ name: "verify_tools", duration_ms: elapsedMs(stepStarted) });
692
+ }
344
693
  if (!quickSteps.triage_welcome) {
694
+ const stepStarted = performance.now();
345
695
  const captured = [];
346
696
  const triageCommand = ["triage_welcome.run_default_mode", "--project-root", projectRoot];
347
697
  try {
@@ -350,6 +700,13 @@ export function runSessionStart(projectRoot, options = {}) {
350
700
  const outcome = runDefaultMode(root, {
351
701
  output: welcomeOpts.output,
352
702
  writeHistory: welcomeOpts.writeHistory,
703
+ now: welcomeOpts.now,
704
+ // #2991: default hot path skips ensureTriageCacheHydrated / maybeSelfHealCache.
705
+ selfHealFn: allowOptionalNetwork
706
+ ? undefined
707
+ : () => {
708
+ /* no network cache work on hot path */
709
+ },
353
710
  });
354
711
  return { exitCode: outcome.exitCode };
355
712
  });
@@ -360,33 +717,51 @@ export function runSessionStart(projectRoot, options = {}) {
360
717
  });
361
718
  const ok = outcome.exitCode === 0;
362
719
  const message = captured.join("\n").trim() || "triage welcome completed";
720
+ const durationMs = elapsedMs(stepStarted);
363
721
  quickSteps.triage_welcome = ritualStep({
364
722
  ok,
365
723
  ts: instant,
366
724
  message,
367
725
  exitCode: outcome.exitCode,
368
726
  command: triageCommand,
727
+ durationMs,
369
728
  });
729
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
370
730
  lines.push(...captured);
371
731
  }
372
732
  catch (exc) {
373
733
  const message = `triage welcome failed: ${String(exc)}`;
734
+ const durationMs = elapsedMs(stepStarted);
374
735
  quickSteps.triage_welcome = ritualStep({
375
736
  ok: false,
376
737
  ts: instant,
377
738
  message,
378
739
  exitCode: 2,
379
740
  command: triageCommand,
741
+ durationMs,
380
742
  });
743
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
381
744
  lines.push(message);
382
745
  }
383
746
  }
384
- try {
385
- const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
386
- lines.push(...releaseAvailability.lines);
747
+ else {
748
+ stepTimings.push({ name: "triage_welcome", duration_ms: 0, skipped: true });
387
749
  }
388
- catch {
389
- // Release availability is a best-effort operator advisory, never a session blocker.
750
+ // #2991: npm release probe is optional network — off by default so ritual write is not blocked.
751
+ if (allowOptionalNetwork) {
752
+ const stepStarted = performance.now();
753
+ try {
754
+ const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
755
+ lines.push(...releaseAvailability.lines);
756
+ }
757
+ catch {
758
+ // Release availability is a best-effort operator advisory, never a session blocker.
759
+ }
760
+ stepTimings.push({ name: "release_probe", duration_ms: elapsedMs(stepStarted) });
761
+ }
762
+ else {
763
+ stepTimings.push({ name: "release_probe", duration_ms: 0, skipped: true });
764
+ lines.push(OPTIONAL_NETWORK_SKIPPED_MESSAGE);
390
765
  }
391
766
  try {
392
767
  const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
@@ -422,6 +797,7 @@ export function runSessionStart(projectRoot, options = {}) {
422
797
  if (consentPrompt.length > 0) {
423
798
  lines.push(consentPrompt.trimEnd());
424
799
  }
800
+ const writeStarted = performance.now();
425
801
  const payload = newRitualStatePayload({
426
802
  sessionId: (options.newSessionId ?? randomUUID)(),
427
803
  gitHead: gitHeadValue,
@@ -430,17 +806,39 @@ export function runSessionStart(projectRoot, options = {}) {
430
806
  quickSteps,
431
807
  gatedSteps,
432
808
  });
433
- const statePath = writeRitualState(projectRoot, payload);
809
+ let statePath;
810
+ try {
811
+ statePath = writeRitualState(projectRoot, payload);
812
+ }
813
+ catch (cause) {
814
+ // #2994: still record failed attempt when ritual-state write throws.
815
+ stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
816
+ emitSessionStartProcessCost({
817
+ ceremonyTier: COLD_CEREMONY_TIER,
818
+ durationMs: elapsedMs(overallStarted),
819
+ exitCode: 2,
820
+ ready: false,
821
+ optionalNetwork: allowOptionalNetwork,
822
+ steps: stepTimings,
823
+ }, { projectRoot });
824
+ throw cause;
825
+ }
826
+ stepTimings.push({ name: "ritual_write", duration_ms: elapsedMs(writeStarted) });
434
827
  const failed = Object.entries(quickSteps)
435
828
  .filter(([, step]) => !step.ok && !step.deferred_reason)
436
829
  .map(([name]) => name);
437
830
  const code = failed.length > 0 ? 1 : 0;
831
+ const totalMs = elapsedMs(overallStarted);
438
832
  const resultPayload = {
439
833
  ready: code === 0,
440
834
  exit_code: code,
835
+ ceremony_tier: COLD_CEREMONY_TIER,
441
836
  state_path: statePath,
442
837
  quick_steps: quickSteps,
443
838
  gated_steps: gatedSteps,
839
+ steps: stepTimings,
840
+ duration_ms: totalMs,
841
+ optional_network: allowOptionalNetwork,
444
842
  user_md: {
445
843
  path: userMd.path,
446
844
  rung: userMd.rung,
@@ -448,8 +846,18 @@ export function runSessionStart(projectRoot, options = {}) {
448
846
  diagnostic: userMd.diagnostic,
449
847
  },
450
848
  environment: environmentContextToDict(environment),
849
+ scm: scmReadinessToDict(scm),
451
850
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
452
851
  };
852
+ // #2994: local process-cost event (best-effort; never blocks ceremony).
853
+ emitSessionStartProcessCost({
854
+ ceremonyTier: COLD_CEREMONY_TIER,
855
+ durationMs: totalMs,
856
+ exitCode: code,
857
+ ready: code === 0,
858
+ optionalNetwork: allowOptionalNetwork,
859
+ steps: stepTimings,
860
+ }, { projectRoot });
453
861
  return { code, payload: resultPayload, lines };
454
862
  }
455
863
  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;