@deftai/directive-core 0.88.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 (202) hide show
  1. package/dist/authz/actions.d.ts +55 -0
  2. package/dist/authz/actions.js +125 -0
  3. package/dist/authz/classify.d.ts +23 -0
  4. package/dist/authz/classify.js +217 -0
  5. package/dist/authz/closed-verb.d.ts +42 -0
  6. package/dist/authz/closed-verb.js +279 -0
  7. package/dist/authz/evaluate.d.ts +41 -0
  8. package/dist/authz/evaluate.js +298 -0
  9. package/dist/authz/index.d.ts +15 -0
  10. package/dist/authz/index.js +15 -0
  11. package/dist/authz/origin.d.ts +28 -0
  12. package/dist/authz/origin.js +64 -0
  13. package/dist/authz/paths.d.ts +6 -0
  14. package/dist/authz/paths.js +19 -0
  15. package/dist/authz/store.d.ts +40 -0
  16. package/dist/authz/store.js +317 -0
  17. package/dist/authz/templates.d.ts +139 -0
  18. package/dist/authz/templates.js +241 -0
  19. package/dist/authz/types.d.ts +111 -0
  20. package/dist/authz/types.js +41 -0
  21. package/dist/authz/verb-classification.d.ts +44 -0
  22. package/dist/authz/verb-classification.js +237 -0
  23. package/dist/branch/evaluate.js +6 -1
  24. package/dist/cache/fetch.js +10 -5
  25. package/dist/cache/io.d.ts +9 -2
  26. package/dist/cache/io.js +30 -10
  27. package/dist/cache/task-cache/store.js +11 -7
  28. package/dist/capacity/backfill.js +10 -4
  29. package/dist/category-b-namespace/index.js +10 -4
  30. package/dist/codebase/default-extractor.js +3 -0
  31. package/dist/codebase/map.js +11 -4
  32. package/dist/doctor/constants.d.ts +1 -1
  33. package/dist/doctor/constants.js +2 -0
  34. package/dist/doctor/doctor-state.js +28 -4
  35. package/dist/doctor/flags.js +21 -1
  36. package/dist/doctor/index.d.ts +1 -0
  37. package/dist/doctor/index.js +1 -0
  38. package/dist/doctor/main.d.ts +10 -0
  39. package/dist/doctor/main.js +182 -0
  40. package/dist/doctor/openclaw-skills.d.ts +109 -0
  41. package/dist/doctor/openclaw-skills.js +463 -0
  42. package/dist/doctor/types.d.ts +22 -0
  43. package/dist/escalation/actions.d.ts +63 -0
  44. package/dist/escalation/actions.js +137 -0
  45. package/dist/escalation/index.d.ts +8 -0
  46. package/dist/escalation/index.js +8 -0
  47. package/dist/escalation/paths.d.ts +3 -0
  48. package/dist/escalation/paths.js +10 -0
  49. package/dist/escalation/store.d.ts +17 -0
  50. package/dist/escalation/store.js +172 -0
  51. package/dist/escalation/types.d.ts +73 -0
  52. package/dist/escalation/types.js +43 -0
  53. package/dist/eval/crud-telemetry.js +30 -10
  54. package/dist/eval/health.js +22 -7
  55. package/dist/eval/readback.js +11 -10
  56. package/dist/eval/run.js +32 -16
  57. package/dist/events/attribution-enrichment.js +10 -4
  58. package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
  59. package/dist/finish-loop/directive-finish-loop.js +239 -0
  60. package/dist/finish-loop/grant-gate.d.ts +31 -0
  61. package/dist/finish-loop/grant-gate.js +169 -0
  62. package/dist/finish-loop/index.d.ts +11 -0
  63. package/dist/finish-loop/index.js +11 -0
  64. package/dist/finish-loop/main.d.ts +35 -0
  65. package/dist/finish-loop/main.js +365 -0
  66. package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
  67. package/dist/finish-loop/pr-finish-loop.js +217 -0
  68. package/dist/finish-loop/progress.d.ts +17 -0
  69. package/dist/finish-loop/progress.js +45 -0
  70. package/dist/finish-loop/queue.d.ts +16 -0
  71. package/dist/finish-loop/queue.js +65 -0
  72. package/dist/finish-loop/types.d.ts +52 -0
  73. package/dist/finish-loop/types.js +10 -0
  74. package/dist/fs/contained-write.d.ts +101 -0
  75. package/dist/fs/contained-write.js +281 -0
  76. package/dist/hooks/classify/classify.d.ts +10 -0
  77. package/dist/hooks/classify/classify.js +37 -0
  78. package/dist/hooks/classify/index.d.ts +14 -0
  79. package/dist/hooks/classify/index.js +13 -0
  80. package/dist/hooks/classify/paths.d.ts +22 -0
  81. package/dist/hooks/classify/paths.js +72 -0
  82. package/dist/hooks/classify/payload.d.ts +16 -0
  83. package/dist/hooks/classify/payload.js +45 -0
  84. package/dist/hooks/classify/stdin.d.ts +12 -0
  85. package/dist/hooks/classify/stdin.js +63 -0
  86. package/dist/hooks/classify/tool-name.d.ts +18 -0
  87. package/dist/hooks/classify/tool-name.js +85 -0
  88. package/dist/hooks/classify/types.d.ts +41 -0
  89. package/dist/hooks/classify/types.js +7 -0
  90. package/dist/hooks/dispatcher.d.ts +21 -18
  91. package/dist/hooks/dispatcher.js +232 -168
  92. package/dist/hooks/fixtures/cases.d.ts +44 -0
  93. package/dist/hooks/fixtures/cases.js +595 -0
  94. package/dist/hooks/fixtures/index.d.ts +2 -0
  95. package/dist/hooks/fixtures/index.js +2 -0
  96. package/dist/hooks/index.d.ts +2 -0
  97. package/dist/hooks/index.js +2 -0
  98. package/dist/index.d.ts +5 -0
  99. package/dist/index.js +6 -0
  100. package/dist/init-deposit/agent-hooks.js +27 -8
  101. package/dist/init-deposit/gitignore.js +11 -3
  102. package/dist/init-deposit/headless-manifest.js +12 -4
  103. package/dist/init-deposit/migrate.d.ts +1 -1
  104. package/dist/init-deposit/migrate.js +13 -3
  105. package/dist/init-deposit/prettierignore.js +10 -3
  106. package/dist/init-deposit/scaffold.js +35 -26
  107. package/dist/init-deposit/xbrief-projections.js +11 -4
  108. package/dist/intake/candidates-log.js +46 -35
  109. package/dist/intake/github-body-cli.d.ts +6 -0
  110. package/dist/intake/github-body-cli.js +17 -0
  111. package/dist/intake/github-body.d.ts +46 -3
  112. package/dist/intake/github-body.js +191 -20
  113. package/dist/intake/issue-emit.d.ts +1 -0
  114. package/dist/intake/issue-emit.js +41 -23
  115. package/dist/lifecycle/event-detect.js +12 -4
  116. package/dist/lifecycle/events.js +31 -20
  117. package/dist/lifecycle/index.d.ts +1 -0
  118. package/dist/lifecycle/index.js +1 -0
  119. package/dist/lifecycle/lifecycle-hygiene.js +9 -3
  120. package/dist/lifecycle/stats.d.ts +53 -0
  121. package/dist/lifecycle/stats.js +286 -0
  122. package/dist/packs/pack-render.js +19 -6
  123. package/dist/plan-sequence/store.js +11 -4
  124. package/dist/policy/hotfix-criteria.d.ts +79 -0
  125. package/dist/policy/hotfix-criteria.js +197 -0
  126. package/dist/policy/index.d.ts +4 -0
  127. package/dist/policy/index.js +31 -1
  128. package/dist/policy/intent-ceiling.d.ts +79 -0
  129. package/dist/policy/intent-ceiling.js +181 -0
  130. package/dist/policy/no-deft-directive.js +10 -3
  131. package/dist/policy/org-force-on-migration.js +9 -5
  132. package/dist/policy/require-human-merge.d.ts +75 -0
  133. package/dist/policy/require-human-merge.js +324 -0
  134. package/dist/policy/resolve.js +17 -6
  135. package/dist/policy/runtime-authority.d.ts +15 -2
  136. package/dist/policy/runtime-authority.js +23 -3
  137. package/dist/policy/write-fence.d.ts +78 -0
  138. package/dist/policy/write-fence.js +164 -0
  139. package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
  140. package/dist/pr-wait-mergeable/cascade.js +28 -0
  141. package/dist/preflight/evaluate.js +10 -0
  142. package/dist/product-signal/consent.js +21 -5
  143. package/dist/product-signal/submit.js +22 -12
  144. package/dist/release/build-dist-runner.js +5 -2
  145. package/dist/release/build-dist.d.ts +19 -1
  146. package/dist/release/build-dist.js +50 -2
  147. package/dist/release/native-steps.js +7 -2
  148. package/dist/release/spawn.js +13 -0
  149. package/dist/release-e2e/git-ops.d.ts +7 -0
  150. package/dist/release-e2e/git-ops.js +35 -2
  151. package/dist/release-publish/pipeline.d.ts +13 -0
  152. package/dist/release-publish/pipeline.js +46 -1
  153. package/dist/scope/audit-log.js +19 -24
  154. package/dist/scope/decompose.js +10 -5
  155. package/dist/scope/decomposed-refs.js +18 -3
  156. package/dist/scope/demote.js +9 -2
  157. package/dist/scope/undo.js +9 -2
  158. package/dist/session/index.d.ts +2 -0
  159. package/dist/session/index.js +2 -0
  160. package/dist/session/process-cost-constants.d.ts +9 -0
  161. package/dist/session/process-cost-constants.js +11 -0
  162. package/dist/session/process-cost.d.ts +53 -0
  163. package/dist/session/process-cost.js +82 -0
  164. package/dist/session/release-availability.js +26 -6
  165. package/dist/session/ritual-sentinel.d.ts +5 -0
  166. package/dist/session/ritual-sentinel.js +31 -13
  167. package/dist/session/session-ready.d.ts +67 -0
  168. package/dist/session/session-ready.js +264 -0
  169. package/dist/session/session-start.d.ts +56 -1
  170. package/dist/session/session-start.js +384 -23
  171. package/dist/session/verify-session-ritual.d.ts +8 -0
  172. package/dist/session/verify-session-ritual.js +95 -35
  173. package/dist/staleness-tickler/state.js +26 -6
  174. package/dist/swarm/launch.js +13 -3
  175. package/dist/swarm/routing.js +9 -3
  176. package/dist/task-surface/index.js +24 -9
  177. package/dist/tool-events/classify.d.ts +20 -0
  178. package/dist/tool-events/classify.js +634 -0
  179. package/dist/tool-events/index.d.ts +10 -0
  180. package/dist/tool-events/index.js +10 -0
  181. package/dist/tool-events/summarize.d.ts +34 -0
  182. package/dist/tool-events/summarize.js +93 -0
  183. package/dist/tool-events/types.d.ts +52 -0
  184. package/dist/tool-events/types.js +13 -0
  185. package/dist/triage/bootstrap/gitignore.js +53 -10
  186. package/dist/triage/cache-path.js +10 -3
  187. package/dist/triage/welcome/summary.js +11 -4
  188. package/dist/triage/welcome/writers.js +45 -16
  189. package/dist/validate-content/validate-links.js +5 -0
  190. package/dist/value/readback.js +11 -6
  191. package/dist/vbrief-activate/activate.js +12 -4
  192. package/dist/vbrief-build/project-definition-io.js +14 -6
  193. package/dist/verify-source/contained-writes.d.ts +59 -0
  194. package/dist/verify-source/contained-writes.js +272 -0
  195. package/dist/verify-source/index.d.ts +1 -0
  196. package/dist/verify-source/index.js +1 -0
  197. package/dist/verify-source/openclaw-tier1.js +14 -1
  198. package/dist/verify-source/verify-stubs.js +3 -0
  199. package/dist/xbrief-migrate/migrate-project.js +26 -12
  200. package/dist/xbrief-migrate/transforms.d.ts +4 -2
  201. package/dist/xbrief-migrate/transforms.js +37 -14
  202. package/package.json +19 -3
@@ -5,23 +5,40 @@ import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposi
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
6
  import { disclosureLine } from "../policy/disclosure.js";
7
7
  import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_FLAG_NAME, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_POLICY, } from "../policy/no-deft-directive.js";
8
+ import { humanMergeDisclosureLine, resolveHumanMergePolicy, } from "../policy/require-human-merge.js";
8
9
  import { resolvePolicy } from "../policy/resolve.js";
9
10
  import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
11
+ import { formatFrameworkCommand } from "../render/framework-commands.js";
10
12
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
11
13
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
12
14
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
13
15
  import { emitSessionValueReadback } from "../value/readback.js";
14
16
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
15
- import { defaultGitRunner, gitHead, worktreePath } from "./git.js";
17
+ import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
18
+ import { emitSessionStartProcessCost } from "./process-cost.js";
16
19
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
17
- import { newRitualStatePayload, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
20
+ import { newRitualStatePayload, readRitualState, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
18
21
  export const SESSION_POSTURES = ["read-only", "mutation"];
19
22
  export const READ_ONLY_POSTURE = "read-only";
20
23
  export const MUTATION_POSTURE = "mutation";
21
24
  export const READ_ONLY_ALIGNMENT_MESSAGE = "Deft Directive active -- AGENTS.md loaded.";
22
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";
23
30
  export const QUICK_STEPS = ["alignment", "branch_policy", "triage_welcome"];
24
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:";
25
42
  const STEP_ALIASES = {
26
43
  branch: "branch_policy",
27
44
  "branch-policy": "branch_policy",
@@ -30,6 +47,89 @@ const STEP_ALIASES = {
30
47
  triage: "triage_welcome",
31
48
  "triage-welcome": "triage_welcome",
32
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
+ }
33
133
  function normaliseStepName(name) {
34
134
  return STEP_ALIASES[name] ?? name;
35
135
  }
@@ -228,12 +328,174 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
228
328
  };
229
329
  return { code: 0, payload: resultPayload, lines };
230
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
+ }
231
492
  export function runSessionStart(projectRoot, options = {}) {
232
493
  const posture = options.posture ?? MUTATION_POSTURE;
233
494
  const instant = options.now ?? new Date();
234
495
  const deferrals = options.deferrals ?? {};
235
496
  const runGit = options.runGit ?? defaultGitRunner;
236
497
  const environment = (options.probeEnvironment ?? detectEnvironmentContext)();
498
+ const ceremonyTier = options.ceremonyTier ?? COLD_CEREMONY_TIER;
237
499
  // #2926: official root opt-out wins locally — skip Directive session ritual.
238
500
  // disabled = skip ritual (exit 0 clean / 1 inconsistent). ready stays false so
239
501
  // automation does not treat opt-out as "session fully initialized for work".
@@ -266,13 +528,30 @@ export function runSessionStart(projectRoot, options = {}) {
266
528
  if (posture === READ_ONLY_POSTURE) {
267
529
  return runReadOnlySessionStart(projectRoot, options, instant, environment);
268
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);
269
538
  const { head: gitHeadValue, error: gitError } = gitHead(projectRoot, runGit);
270
539
  if (gitHeadValue === null) {
271
540
  const payload = {
272
541
  ready: false,
542
+ exit_code: 2,
543
+ ceremony_tier: COLD_CEREMONY_TIER,
273
544
  environment: environmentContextToDict(environment),
274
545
  message: gitError ?? "could not resolve git HEAD",
275
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 });
276
555
  return {
277
556
  code: 2,
278
557
  payload,
@@ -289,6 +568,7 @@ export function runSessionStart(projectRoot, options = {}) {
289
568
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
290
569
  const userMd = resolveUserMd(projectRoot);
291
570
  if (!quickSteps.alignment) {
571
+ const stepStarted = performance.now();
292
572
  const message = "Deft Directive active -- AGENTS.md loaded.";
293
573
  // Sanitize newlines on the data-derived path/diagnostic before they land in
294
574
  // the ritual-step message / terminal output (matches doctor's CWE-116
@@ -299,42 +579,66 @@ export function runSessionStart(projectRoot, options = {}) {
299
579
  const userMdLine = userMd.found
300
580
  ? `USER.md resolved (${userMd.rung}): ${safePath}`
301
581
  : safeDiagnostic;
582
+ const durationMs = elapsedMs(stepStarted);
302
583
  quickSteps.alignment = ritualStep({
303
584
  ok: true,
304
585
  ts: instant,
305
586
  message: `${message} ${userMdLine}`,
587
+ durationMs,
306
588
  });
589
+ stepTimings.push({ name: "alignment", duration_ms: durationMs });
307
590
  lines.push(message);
308
591
  lines.push(userMdLine);
309
592
  }
593
+ else {
594
+ stepTimings.push({ name: "alignment", duration_ms: 0, skipped: true });
595
+ }
310
596
  lines.push(formatEnvironmentContext(environment));
311
597
  if (!quickSteps.branch_policy) {
598
+ const stepStarted = performance.now();
312
599
  const result = resolvePolicy(projectRoot);
313
600
  const message = disclosureLine(result);
314
601
  const ok = result.error === null || result.source === "default-fail-closed";
602
+ lines.push(message);
603
+ // Human merge gate disclosure (#1193) — surface when ON (or env bypass active).
604
+ const humanMerge = resolveHumanMergePolicy(projectRoot);
605
+ const humanMergeLine = humanMergeDisclosureLine(humanMerge);
606
+ if (humanMergeLine !== null) {
607
+ lines.push(humanMergeLine);
608
+ }
609
+ const branchSync = defaultBranchSync(projectRoot, runGit);
610
+ if (branchSync.warning) {
611
+ lines.push(branchSync.warning);
612
+ }
613
+ const durationMs = elapsedMs(stepStarted);
315
614
  quickSteps.branch_policy = ritualStep({
316
615
  ok,
317
616
  ts: instant,
318
617
  message,
319
618
  exitCode: ok ? 0 : 2,
619
+ durationMs,
320
620
  });
321
- lines.push(message);
322
- const branchSync = defaultBranchSync(projectRoot, runGit);
323
- if (branchSync.warning) {
324
- lines.push(branchSync.warning);
325
- }
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) });
326
639
  }
327
- const verifyToolsFn = options.verifyTools ??
328
- ((output) => {
329
- const toolLines = [];
330
- const result = verifyRequiredTools({ outputFn: (line) => toolLines.push(line) });
331
- for (const line of toolLines) {
332
- output(line);
333
- }
334
- return { exitCode: result.exitCode };
335
- });
336
- verifyToolsFn((line) => lines.push(line));
337
640
  if (!quickSteps.triage_welcome) {
641
+ const stepStarted = performance.now();
338
642
  const captured = [];
339
643
  const triageCommand = ["triage_welcome.run_default_mode", "--project-root", projectRoot];
340
644
  try {
@@ -343,6 +647,13 @@ export function runSessionStart(projectRoot, options = {}) {
343
647
  const outcome = runDefaultMode(root, {
344
648
  output: welcomeOpts.output,
345
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
+ },
346
657
  });
347
658
  return { exitCode: outcome.exitCode };
348
659
  });
@@ -353,33 +664,51 @@ export function runSessionStart(projectRoot, options = {}) {
353
664
  });
354
665
  const ok = outcome.exitCode === 0;
355
666
  const message = captured.join("\n").trim() || "triage welcome completed";
667
+ const durationMs = elapsedMs(stepStarted);
356
668
  quickSteps.triage_welcome = ritualStep({
357
669
  ok,
358
670
  ts: instant,
359
671
  message,
360
672
  exitCode: outcome.exitCode,
361
673
  command: triageCommand,
674
+ durationMs,
362
675
  });
676
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
363
677
  lines.push(...captured);
364
678
  }
365
679
  catch (exc) {
366
680
  const message = `triage welcome failed: ${String(exc)}`;
681
+ const durationMs = elapsedMs(stepStarted);
367
682
  quickSteps.triage_welcome = ritualStep({
368
683
  ok: false,
369
684
  ts: instant,
370
685
  message,
371
686
  exitCode: 2,
372
687
  command: triageCommand,
688
+ durationMs,
373
689
  });
690
+ stepTimings.push({ name: "triage_welcome", duration_ms: durationMs });
374
691
  lines.push(message);
375
692
  }
376
693
  }
377
- try {
378
- const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
379
- lines.push(...releaseAvailability.lines);
694
+ else {
695
+ stepTimings.push({ name: "triage_welcome", duration_ms: 0, skipped: true });
380
696
  }
381
- catch {
382
- // 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);
383
712
  }
384
713
  try {
385
714
  const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
@@ -415,6 +744,7 @@ export function runSessionStart(projectRoot, options = {}) {
415
744
  if (consentPrompt.length > 0) {
416
745
  lines.push(consentPrompt.trimEnd());
417
746
  }
747
+ const writeStarted = performance.now();
418
748
  const payload = newRitualStatePayload({
419
749
  sessionId: (options.newSessionId ?? randomUUID)(),
420
750
  gitHead: gitHeadValue,
@@ -423,17 +753,39 @@ export function runSessionStart(projectRoot, options = {}) {
423
753
  quickSteps,
424
754
  gatedSteps,
425
755
  });
426
- 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) });
427
774
  const failed = Object.entries(quickSteps)
428
775
  .filter(([, step]) => !step.ok && !step.deferred_reason)
429
776
  .map(([name]) => name);
430
777
  const code = failed.length > 0 ? 1 : 0;
778
+ const totalMs = elapsedMs(overallStarted);
431
779
  const resultPayload = {
432
780
  ready: code === 0,
433
781
  exit_code: code,
782
+ ceremony_tier: COLD_CEREMONY_TIER,
434
783
  state_path: statePath,
435
784
  quick_steps: quickSteps,
436
785
  gated_steps: gatedSteps,
786
+ steps: stepTimings,
787
+ duration_ms: totalMs,
788
+ optional_network: allowOptionalNetwork,
437
789
  user_md: {
438
790
  path: userMd.path,
439
791
  rung: userMd.rung,
@@ -443,6 +795,15 @@ export function runSessionStart(projectRoot, options = {}) {
443
795
  environment: environmentContextToDict(environment),
444
796
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
445
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 });
446
807
  return { code, payload: resultPayload, lines };
447
808
  }
448
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;