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