@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
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
4
4
  import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
5
5
  import { canonicalLogPath, readAll } from "./audit-log.js";
6
+ import { batchPromote } from "./batch-promote.js";
6
7
  import { TRANSITIONS } from "./constants.js";
7
8
  import { batchDemote, DEFAULT_OLDER_THAN_DAYS, demoteOne, resolveDemoteFilePath, resolveFilePath, resolveProjectRootStrict, } from "./demote.js";
8
9
  import { completedPathForScopeMove, findOpenUmbrellaReferences, renderOpenUmbrellaWarning, } from "./open-umbrella-warning.js";
@@ -10,12 +11,13 @@ import { resolveProjectRoot } from "./project-context.js";
10
11
  import { recordWipCapOverride, runTransition } from "./transition.js";
11
12
  import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
12
13
  import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
13
- const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force]\n" +
14
+ const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force] [--batch]\n" +
14
15
  " {activate,block,cancel,complete,fail,promote,restore,unblock}\n" +
15
- " file\n" +
16
- "scope_lifecycle.py: error: the following arguments are required: action, file\n";
16
+ " [file ...]\n" +
17
+ "scope_lifecycle.py: error: the following arguments are required: action, file\n" +
18
+ "(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n";
17
19
  function parseLifecycleArgv(argv) {
18
- if (argv.length < 2) {
20
+ if (argv.length < 1) {
19
21
  return { args: null, error: "usage" };
20
22
  }
21
23
  const action = argv[0] ?? "";
@@ -25,6 +27,8 @@ function parseLifecycleArgv(argv) {
25
27
  let file = "";
26
28
  let projectRoot;
27
29
  let force = false;
30
+ let batch = false;
31
+ const batchFiles = [];
28
32
  for (let i = 1; i < argv.length; i += 1) {
29
33
  const arg = argv[i];
30
34
  if (arg === undefined) {
@@ -33,6 +37,9 @@ function parseLifecycleArgv(argv) {
33
37
  if (arg === "--force") {
34
38
  force = true;
35
39
  }
40
+ else if (arg === "--batch") {
41
+ batch = true;
42
+ }
36
43
  else if (arg === "--project-root") {
37
44
  projectRoot = argv[i + 1];
38
45
  i += 1;
@@ -40,13 +47,36 @@ function parseLifecycleArgv(argv) {
40
47
  else if (arg?.startsWith("--project-root=")) {
41
48
  projectRoot = arg.slice("--project-root=".length);
42
49
  }
43
- else if (!arg.startsWith("-") && file.length === 0) {
44
- file = arg;
50
+ else if (!arg.startsWith("-")) {
51
+ if (batch) {
52
+ batchFiles.push(arg);
53
+ }
54
+ else if (file.length === 0) {
55
+ file = arg;
56
+ }
57
+ else {
58
+ return { args: null, error: "usage" };
59
+ }
45
60
  }
46
61
  else {
47
62
  return { args: null, error: "usage" };
48
63
  }
49
64
  }
65
+ if (batch) {
66
+ if (action !== "promote") {
67
+ return { args: null, error: "usage" };
68
+ }
69
+ return {
70
+ args: {
71
+ action,
72
+ file: file.length > 0 ? file : "",
73
+ projectRoot,
74
+ force,
75
+ batch: true,
76
+ batchFiles: batchFiles.length > 0 ? batchFiles : file.length > 0 ? [file] : [],
77
+ },
78
+ };
79
+ }
50
80
  if (file.length === 0) {
51
81
  return { args: null, error: "usage" };
52
82
  }
@@ -61,7 +91,28 @@ export function lifecycleMain(argv) {
61
91
  }
62
92
  return 2;
63
93
  }
64
- const { action, file, projectRoot, force } = parsed.args;
94
+ const { action, file, projectRoot, force, batch, batchFiles } = parsed.args;
95
+ if (batch === true && action === "promote") {
96
+ const result = batchPromote({
97
+ files: batchFiles && batchFiles.length > 0 ? batchFiles : undefined,
98
+ force: force === true,
99
+ projectRoot,
100
+ });
101
+ for (const line of result.messages) {
102
+ process.stdout.write(`${line}\n`);
103
+ }
104
+ for (const line of result.skipped) {
105
+ process.stdout.write(` skipped: ${line}\n`);
106
+ }
107
+ if (!result.ok && result.exitCode !== 0) {
108
+ for (const line of result.messages) {
109
+ if (line.startsWith("ERROR:") || line.includes("WIP cap")) {
110
+ process.stderr.write(`${line}\n`);
111
+ }
112
+ }
113
+ }
114
+ return result.exitCode;
115
+ }
65
116
  const [filePath, error] = resolveFilePath(file, projectRoot);
66
117
  if (error !== null || filePath === null) {
67
118
  process.stderr.write(`Error: ${error}\n`);
@@ -7,5 +7,11 @@ export interface WipCapCheck {
7
7
  }
8
8
  /** Resolve WIP cap and decide if promotion is allowed (#1124). */
9
9
  export declare function checkWipCap(projectRoot: string, force?: boolean): WipCapCheck;
10
+ /**
11
+ * WIP check for promoting `additional` scopes in one batch (#3011).
12
+ * Single promote is `additional = 1` (same as historic count >= cap refuse).
13
+ * Batch of N refuses when `count + N > cap` unless `--force`.
14
+ */
15
+ export declare function checkWipCapForAdditional(projectRoot: string, additional: number, force?: boolean): WipCapCheck;
10
16
  export declare function formatWipCapRefusal(check: WipCapCheck): string;
11
17
  //# sourceMappingURL=wip-cap-check.d.ts.map
@@ -1,10 +1,21 @@
1
1
  import { countVbriefWip, resolveWipCap } from "../policy/wip.js";
2
2
  /** Resolve WIP cap and decide if promotion is allowed (#1124). */
3
3
  export function checkWipCap(projectRoot, force = false) {
4
+ return checkWipCapForAdditional(projectRoot, 1, force);
5
+ }
6
+ /**
7
+ * WIP check for promoting `additional` scopes in one batch (#3011).
8
+ * Single promote is `additional = 1` (same as historic count >= cap refuse).
9
+ * Batch of N refuses when `count + N > cap` unless `--force`.
10
+ */
11
+ export function checkWipCapForAdditional(projectRoot, additional, force = false) {
4
12
  const capResult = resolveWipCap(projectRoot);
5
13
  const cap = capResult.cap;
6
14
  const count = countVbriefWip(projectRoot);
7
- const overCap = count >= cap;
15
+ const add = Math.max(0, Math.floor(additional));
16
+ // Single-promote parity: refuse when already at/over cap before adding one.
17
+ // Batch: refuse when current + additional would exceed the cap.
18
+ const overCap = add <= 1 ? count >= cap : count + add > cap;
8
19
  if (!overCap) {
9
20
  return { allowed: true, cap, count, source: capResult.source, forceOverride: false };
10
21
  }
@@ -18,6 +29,7 @@ export function formatWipCapRefusal(check) {
18
29
  "Either:\n" +
19
30
  " task scope:demote <existing> # return one to proposed/\n" +
20
31
  " task scope:demote --batch --older-than-days 30 # bulk relief (D9 folded into D1)\n" +
21
- " task scope:promote <file> --force # override (logged)");
32
+ " task scope:promote <file> --force # override (logged)\n" +
33
+ " task scope:promote --batch --force # batch override (#3011)");
22
34
  }
23
35
  //# sourceMappingURL=wip-cap-check.js.map
@@ -1,8 +1,10 @@
1
1
  export * from "./git.js";
2
2
  export * from "./json.js";
3
3
  export * from "./posture.js";
4
+ export * from "./process-cost.js";
4
5
  export * from "./resume-conditions.js";
5
6
  export * from "./ritual-sentinel.js";
7
+ export * from "./session-ready.js";
6
8
  export * from "./session-start.js";
7
9
  export * from "./session-start-hook.js";
8
10
  export * from "./time.js";
@@ -1,8 +1,10 @@
1
1
  export * from "./git.js";
2
2
  export * from "./json.js";
3
3
  export * from "./posture.js";
4
+ export * from "./process-cost.js";
4
5
  export * from "./resume-conditions.js";
5
6
  export * from "./ritual-sentinel.js";
7
+ export * from "./session-ready.js";
6
8
  export * from "./session-start.js";
7
9
  export * from "./session-start-hook.js";
8
10
  export * from "./time.js";
@@ -0,0 +1,9 @@
1
+ /** Canonical process-cost event names for ceremony observability (#2994). */
2
+ export declare const PROCESS_COST_EVENT_NAMES: {
3
+ readonly sessionStart: "session:start";
4
+ readonly sessionRitualBlocked: "session:ritual-blocked";
5
+ };
6
+ export type ProcessCostEventName = (typeof PROCESS_COST_EVENT_NAMES)[keyof typeof PROCESS_COST_EVENT_NAMES];
7
+ /** Required payload keys per process-cost event (merged into lifecycle/events). */
8
+ export declare const PROCESS_COST_REQUIRED_PAYLOAD: Readonly<Record<string, readonly string[]>>;
9
+ //# sourceMappingURL=process-cost-constants.d.ts.map
@@ -0,0 +1,11 @@
1
+ /** Canonical process-cost event names for ceremony observability (#2994). */
2
+ export const PROCESS_COST_EVENT_NAMES = {
3
+ sessionStart: "session:start",
4
+ sessionRitualBlocked: "session:ritual-blocked",
5
+ };
6
+ /** Required payload keys per process-cost event (merged into lifecycle/events). */
7
+ export const PROCESS_COST_REQUIRED_PAYLOAD = {
8
+ [PROCESS_COST_EVENT_NAMES.sessionStart]: ["ceremony_tier", "duration_ms", "exit_code"],
9
+ [PROCESS_COST_EVENT_NAMES.sessionRitualBlocked]: ["tool_name", "code"],
10
+ };
11
+ //# sourceMappingURL=process-cost-constants.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Local process-cost ceremony events (#2994).
3
+ *
4
+ * Always-on, best-effort appends to `.deft-cache/events.jsonl`.
5
+ * Not gated on valueFeedback (distinct from #1709 attribution).
6
+ * No remote / Product Insights upload (#2603 not required).
7
+ *
8
+ * Types are declared locally (not imported from session-start) so this module
9
+ * does not form an import cycle with session-start call sites.
10
+ */
11
+ import { type BehavioralEventRecord } from "../lifecycle/events.js";
12
+ export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, type ProcessCostEventName, } from "./process-cost-constants.js";
13
+ /** Ceremony tier labels mirror session-start cold|rearm (#2992 / #2994). */
14
+ export type ProcessCostCeremonyTier = "cold" | "rearm";
15
+ export interface ProcessCostStepTiming {
16
+ readonly name: string;
17
+ readonly duration_ms: number;
18
+ readonly skipped?: boolean;
19
+ }
20
+ export interface EmitProcessCostOptions {
21
+ readonly projectRoot: string;
22
+ /**
23
+ * Optional explicit log path for tests.
24
+ * When omitted, `emit` resolves via DEFT_EVENT_LOG then `.deft-cache/events.jsonl`
25
+ * (must not pre-resolve the default here or DEFT_EVENT_LOG is bypassed).
26
+ */
27
+ readonly logPath?: string | null;
28
+ }
29
+ export interface SessionStartProcessCostInput {
30
+ readonly ceremonyTier: ProcessCostCeremonyTier;
31
+ readonly durationMs: number;
32
+ readonly exitCode: number;
33
+ readonly ready?: boolean;
34
+ readonly optionalNetwork?: boolean;
35
+ readonly steps?: readonly ProcessCostStepTiming[];
36
+ }
37
+ export interface SessionRitualBlockedProcessCostInput {
38
+ readonly toolName: string;
39
+ readonly code?: string;
40
+ readonly recoveryTier?: "cold" | "rearm";
41
+ readonly detail?: string;
42
+ }
43
+ /**
44
+ * Emit `session:start` after cold/re-arm ceremony completes (or fails early).
45
+ * Returns null on any failure (telemetry must not interrupt session:start).
46
+ */
47
+ export declare function emitSessionStartProcessCost(input: SessionStartProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
48
+ /**
49
+ * Emit `session:ritual-blocked` on PreToolUse ritual-not-ready deny.
50
+ * Returns null on any failure (telemetry must not change deny verdict).
51
+ */
52
+ export declare function emitSessionRitualBlockedProcessCost(input: SessionRitualBlockedProcessCostInput, options: EmitProcessCostOptions): BehavioralEventRecord | null;
53
+ //# sourceMappingURL=process-cost.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Local process-cost ceremony events (#2994).
3
+ *
4
+ * Always-on, best-effort appends to `.deft-cache/events.jsonl`.
5
+ * Not gated on valueFeedback (distinct from #1709 attribution).
6
+ * No remote / Product Insights upload (#2603 not required).
7
+ *
8
+ * Types are declared locally (not imported from session-start) so this module
9
+ * does not form an import cycle with session-start call sites.
10
+ */
11
+ import { emit } from "../lifecycle/events.js";
12
+ import { PROCESS_COST_EVENT_NAMES } from "./process-cost-constants.js";
13
+ export { PROCESS_COST_EVENT_NAMES, PROCESS_COST_REQUIRED_PAYLOAD, } from "./process-cost-constants.js";
14
+ /**
15
+ * Emit `session:start` after cold/re-arm ceremony completes (or fails early).
16
+ * Returns null on any failure (telemetry must not interrupt session:start).
17
+ */
18
+ export function emitSessionStartProcessCost(input, options) {
19
+ try {
20
+ const payload = {
21
+ ceremony_tier: input.ceremonyTier,
22
+ duration_ms: input.durationMs,
23
+ exit_code: input.exitCode,
24
+ };
25
+ if (input.ready !== undefined) {
26
+ payload.ready = input.ready;
27
+ }
28
+ if (input.optionalNetwork !== undefined) {
29
+ payload.optional_network = input.optionalNetwork;
30
+ }
31
+ if (input.steps !== undefined) {
32
+ payload.steps = input.steps.map((step) => {
33
+ const entry = {
34
+ name: step.name,
35
+ duration_ms: step.duration_ms,
36
+ };
37
+ if (step.skipped === true) {
38
+ entry.skipped = true;
39
+ }
40
+ return entry;
41
+ });
42
+ }
43
+ // Pass logPath only when caller overrides; otherwise emit honors DEFT_EVENT_LOG.
44
+ const emitOptions = { projectRoot: options.projectRoot };
45
+ if (options.logPath !== undefined) {
46
+ emitOptions.logPath = options.logPath;
47
+ }
48
+ return emit(PROCESS_COST_EVENT_NAMES.sessionStart, payload, emitOptions);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Emit `session:ritual-blocked` on PreToolUse ritual-not-ready deny.
56
+ * Returns null on any failure (telemetry must not change deny verdict).
57
+ */
58
+ export function emitSessionRitualBlockedProcessCost(input, options) {
59
+ try {
60
+ const payload = {
61
+ tool_name: input.toolName,
62
+ code: input.code ?? "ritual-not-ready",
63
+ };
64
+ if (input.recoveryTier !== undefined) {
65
+ payload.recovery_tier = input.recoveryTier;
66
+ }
67
+ if (input.detail !== undefined && input.detail.length > 0) {
68
+ // Cap free-form detail so JSONL lines stay bounded.
69
+ payload.detail =
70
+ input.detail.length > 240 ? `${input.detail.slice(0, 237)}...` : input.detail;
71
+ }
72
+ const emitOptions = { projectRoot: options.projectRoot };
73
+ if (options.logPath !== undefined) {
74
+ emitOptions.logPath = options.logPath;
75
+ }
76
+ return emit(PROCESS_COST_EVENT_NAMES.sessionRitualBlocked, payload, emitOptions);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ //# sourceMappingURL=process-cost.js.map
@@ -30,6 +30,8 @@ export interface RitualStepInput {
30
30
  readonly exitCode?: number | null;
31
31
  readonly message?: string | null;
32
32
  readonly command?: readonly string[] | null;
33
+ /** Wall-clock ms for this step when measured (#2991). */
34
+ readonly durationMs?: number | null;
33
35
  }
34
36
  export declare function ritualStatePath(projectRoot: string): string;
35
37
  export declare function ritualStep(input: RitualStepInput): Record<string, unknown>;
@@ -53,10 +55,13 @@ export interface MarkRitualStaleAfterCompactResult {
53
55
  /**
54
56
  * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
55
57
  * Reuses ritual-state age semantics — no parallel policy stack.
58
+ * Marks rearm_needed so recovery messaging prefers session:start --rearm when cold is unnecessary (#2992).
56
59
  */
57
60
  export declare function markRitualStaleAfterCompact(projectRoot: string, input?: {
58
61
  now?: Date;
59
62
  }): MarkRitualStaleAfterCompactResult;
63
+ /** Whether ritual-state.json marks compact/age recovery as re-arm preferred (#2992). */
64
+ export declare function ritualStateMarksRearmNeeded(state: RitualState): boolean;
60
65
  export declare function recordRitualStep(projectRoot: string, input: {
61
66
  tier: "quick" | "gated";
62
67
  stepName: string;
@@ -35,6 +35,9 @@ export function ritualStep(input) {
35
35
  if (input.command && input.command.length > 0) {
36
36
  payload.command = input.command.map(String);
37
37
  }
38
+ if (input.durationMs !== null && input.durationMs !== undefined) {
39
+ payload.duration_ms = Math.max(0, Math.round(input.durationMs));
40
+ }
38
41
  return payload;
39
42
  }
40
43
  export function newRitualStatePayload(input) {
@@ -196,6 +199,7 @@ export const RITUAL_STALE_EPOCH = new Date("1970-01-01T00:00:00Z");
196
199
  /**
197
200
  * Invalidate an existing mutation ritual after host context compaction/resume (#2113).
198
201
  * Reuses ritual-state age semantics — no parallel policy stack.
202
+ * Marks rearm_needed so recovery messaging prefers session:start --rearm when cold is unnecessary (#2992).
199
203
  */
200
204
  export function markRitualStaleAfterCompact(projectRoot, input = {}) {
201
205
  const now = input.now ?? new Date();
@@ -211,14 +215,21 @@ export function markRitualStaleAfterCompact(projectRoot, input = {}) {
211
215
  const payload = { ...state.raw };
212
216
  payload.started_at = timestampIso(RITUAL_STALE_EPOCH);
213
217
  payload.compact_resume_at = timestampIso(now);
218
+ // #2992: compact invalidates the ritual clock only — prefer re-arm recovery when worktree/HEAD allow.
219
+ payload.rearm_needed = true;
214
220
  writeRitualState(projectRoot, payload);
215
221
  return {
216
222
  changed: true,
217
223
  statePath,
218
- message: "Marked session ritual stale after context compaction; run session:start and " +
224
+ message: "Marked session ritual re-arm needed after context compaction; run " +
225
+ "session:start --rearm (or full session:start if worktree/HEAD changed) and " +
219
226
  "verify:session-ritual -- --tier=gated before direct writes.",
220
227
  };
221
228
  }
229
+ /** Whether ritual-state.json marks compact/age recovery as re-arm preferred (#2992). */
230
+ export function ritualStateMarksRearmNeeded(state) {
231
+ return state.raw.rearm_needed === true || typeof state.raw.compact_resume_at === "string";
232
+ }
222
233
  export function recordRitualStep(projectRoot, input) {
223
234
  const [state, err] = readRitualState(projectRoot);
224
235
  if (state === null) {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * session:ready — one-shot mutation recovery (#2993).
3
+ *
4
+ * Composes existing APIs (does not reimplement session:start or gated verify):
5
+ * 1. Fast path when inspectSessionRitual(gated) is already green
6
+ * 2. session:start when quick-tier state is missing/stale
7
+ * 3. verifySessionRitual(--tier=gated) for doctor + cache_fresh
8
+ * 4. cache fetch-all recovery when cache_fresh is the remaining blocker
9
+ * 5. re-verify gated
10
+ *
11
+ * Exit 0 means a subsequent inspect/verify gated would pass (or the single
12
+ * remaining hard blocker is printed once).
13
+ */
14
+ import type { GitRunner } from "./git.js";
15
+ import { type SessionStartOptions, type SessionStartResult } from "./session-start.js";
16
+ import { type InspectSessionRitualOptions, type RitualRunner, type VerifyResult, type VerifySessionRitualOptions } from "./verify-session-ritual.js";
17
+ export declare const SESSION_READY_FAST_PATH = "fast";
18
+ export declare const SESSION_READY_RECOVERED = "recovered";
19
+ export declare const SESSION_READY_VERIFIED = "verified";
20
+ export declare const SESSION_READY_FAILED = "failed";
21
+ export type SessionReadyPath = typeof SESSION_READY_FAST_PATH | typeof SESSION_READY_RECOVERED | typeof SESSION_READY_VERIFIED | typeof SESSION_READY_FAILED;
22
+ export interface SessionReadyResult {
23
+ readonly code: number;
24
+ readonly message: string;
25
+ readonly path: SessionReadyPath;
26
+ readonly lines: readonly string[];
27
+ /** Steps actually executed (for tests / --json). */
28
+ readonly steps: readonly string[];
29
+ readonly duration_ms: number;
30
+ }
31
+ export type CacheFetchAllSeam = (options: {
32
+ source: string;
33
+ repo: string;
34
+ force?: boolean;
35
+ cacheRoot?: string;
36
+ }) => unknown;
37
+ export interface SessionReadyOptions {
38
+ readonly now?: Date;
39
+ readonly runGit?: GitRunner;
40
+ readonly runner?: RitualRunner;
41
+ readonly env?: NodeJS.ProcessEnv;
42
+ /** When set, overrides DEFT_TRIAGE_REPO / git remote inference for cache recovery. */
43
+ readonly repo?: string | null;
44
+ readonly sessionStartOptions?: Omit<SessionStartOptions, "now" | "runGit" | "env">;
45
+ readonly inspectRitual?: (projectRoot: string, options: InspectSessionRitualOptions) => VerifyResult;
46
+ readonly verifyRitual?: (projectRoot: string, options: VerifySessionRitualOptions) => VerifyResult;
47
+ readonly runStart?: (projectRoot: string, options: SessionStartOptions) => SessionStartResult;
48
+ readonly fetchAll?: CacheFetchAllSeam;
49
+ readonly inferRepo?: (projectRoot: string) => string | null;
50
+ /** Skip cache recovery even when cache_fresh failed (tests). */
51
+ readonly skipCacheRecovery?: boolean;
52
+ }
53
+ /** True when a gated verify failure is attributable to the cache_fresh step. */
54
+ export declare function isCacheFreshFailure(message: string): boolean;
55
+ /**
56
+ * True readiness for PreToolUse: exit 0 without DEFT_SESSION_RITUAL_SKIP bypass.
57
+ * Bypassed code 0 does not make inspectSessionRitual green (#2993 Greptile P1).
58
+ */
59
+ export declare function isGatedVerifyActuallyReady(result: VerifyResult): boolean;
60
+ /** Infer OWNER/NAME from DEFT_TRIAGE_REPO or git remote origin. */
61
+ export declare function inferSessionReadyRepo(projectRoot: string, env?: NodeJS.ProcessEnv): string | null;
62
+ /**
63
+ * One-shot recovery: leave the worktree PreToolUse-gated-inspect green, or
64
+ * print the single remaining blocker.
65
+ */
66
+ export declare function runSessionReady(projectRoot: string, options?: SessionReadyOptions): SessionReadyResult;
67
+ //# sourceMappingURL=session-ready.d.ts.map