@deftai/directive-core 0.79.3 → 0.80.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/coverage-hotspots/evaluate.d.ts +49 -0
- package/dist/coverage-hotspots/evaluate.js +262 -0
- package/dist/coverage-hotspots/index.d.ts +3 -0
- package/dist/coverage-hotspots/index.js +3 -0
- package/dist/coverage-hotspots/thresholds.d.ts +5 -0
- package/dist/coverage-hotspots/thresholds.js +48 -0
- package/dist/hooks/dispatcher.d.ts +25 -3
- package/dist/hooks/dispatcher.js +216 -65
- package/dist/hooks/readonly.d.ts +7 -0
- package/dist/hooks/readonly.js +83 -0
- package/dist/hooks/tools.d.ts +7 -0
- package/dist/hooks/tools.js +16 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -1
- package/dist/init-deposit/agent-hooks.js +77 -21
- package/dist/init-deposit/xbrief-projections.d.ts +7 -0
- package/dist/init-deposit/xbrief-projections.js +29 -1
- package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
- package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +31 -1
- package/dist/policy/runtime-authority.d.ts +54 -0
- package/dist/policy/runtime-authority.js +180 -0
- package/dist/policy/staleness-tickler.d.ts +20 -0
- package/dist/policy/staleness-tickler.js +151 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +7 -1
- package/dist/pr-merge-readiness/ci-gate.js +38 -5
- package/dist/pr-merge-readiness/gh.d.ts +4 -0
- package/dist/pr-merge-readiness/gh.js +10 -3
- package/dist/pr-merge-readiness/index.d.ts +2 -1
- package/dist/pr-merge-readiness/index.js +2 -1
- package/dist/pr-merge-readiness/runner-capacity-stall.d.ts +35 -0
- package/dist/pr-merge-readiness/runner-capacity-stall.js +46 -0
- package/dist/pr-wait-mergeable/cascade.js +8 -1
- package/dist/pr-wait-mergeable/classify.js +5 -1
- package/dist/pr-wait-mergeable/wrappers.js +15 -4
- package/dist/pr-watch/constants.d.ts +15 -0
- package/dist/pr-watch/constants.js +16 -1
- package/dist/pr-watch/main.js +12 -0
- package/dist/pr-watch/probe.js +13 -1
- package/dist/pr-watch/types.d.ts +9 -0
- package/dist/pr-watch/watch.js +24 -1
- package/dist/scope/main.js +12 -0
- package/dist/scope/undo.js +12 -2
- package/dist/session/release-availability.d.ts +21 -0
- package/dist/session/release-availability.js +109 -0
- package/dist/session/ritual-sentinel.d.ts +14 -0
- package/dist/session/ritual-sentinel.js +28 -0
- package/dist/session/session-start.d.ts +10 -0
- package/dist/session/session-start.js +18 -0
- package/dist/staleness-tickler/escalation.d.ts +14 -0
- package/dist/staleness-tickler/escalation.js +106 -0
- package/dist/staleness-tickler/idle.d.ts +18 -0
- package/dist/staleness-tickler/idle.js +47 -0
- package/dist/staleness-tickler/index.d.ts +8 -0
- package/dist/staleness-tickler/index.js +8 -0
- package/dist/staleness-tickler/probe-directive.d.ts +32 -0
- package/dist/staleness-tickler/probe-directive.js +106 -0
- package/dist/staleness-tickler/probe-xbrief.d.ts +12 -0
- package/dist/staleness-tickler/probe-xbrief.js +103 -0
- package/dist/staleness-tickler/run.d.ts +31 -0
- package/dist/staleness-tickler/run.js +227 -0
- package/dist/staleness-tickler/state.d.ts +10 -0
- package/dist/staleness-tickler/state.js +46 -0
- package/dist/staleness-tickler/types.d.ts +85 -0
- package/dist/staleness-tickler/types.js +5 -0
- package/dist/triage/bootstrap/cache-module.d.ts +25 -0
- package/dist/triage/bootstrap/cache-module.js +39 -0
- package/dist/triage/bootstrap/index.d.ts +1 -0
- package/dist/triage/bootstrap/index.js +7 -82
- package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
- package/dist/vbrief-validate/plan-hooks.js +50 -0
- package/dist/verify-env/agent-hooks.js +4 -2
- package/dist/xbrief-migrate/agents-header.d.ts +0 -11
- package/dist/xbrief-migrate/agents-header.js +10 -1
- package/package.json +3 -3
package/dist/pr-watch/watch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultRunGh } from "../pr-merge-readiness/gh.js";
|
|
2
|
-
import { DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
|
|
2
|
+
import { DEFAULT_CI_BLOCKED_THRESHOLD, DEFAULT_MAX_WAIT_MINUTES, DEFAULT_POLL_SECONDS, DEFAULT_STALL_THRESHOLD, EXIT_CLEAN, EXIT_NEW_P0_P1, EXIT_TERMINAL_ERROR, VERDICT_CI_BLOCKED, VERDICT_CLEAN, VERDICT_CONFIG, VERDICT_ERRORED, VERDICT_NEW_P0_P1, VERDICT_PENDING, VERDICT_RUNNER_CAPACITY_STALL, VERDICT_STALL, VERDICT_TIMEOUT, } from "./constants.js";
|
|
3
3
|
import { probeOnce } from "./probe.js";
|
|
4
4
|
const systemMonotonicClock = {
|
|
5
5
|
now() {
|
|
@@ -52,6 +52,7 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
52
52
|
const startedAt = clockFn.now();
|
|
53
53
|
let lastProbe = null;
|
|
54
54
|
let stallStreak = 0;
|
|
55
|
+
let ciBlockedStreak = 0;
|
|
55
56
|
const build = (verdict, exitCode, probe, poll) => ({
|
|
56
57
|
verdict,
|
|
57
58
|
exitCode,
|
|
@@ -79,6 +80,25 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
79
80
|
if (probe.errored) {
|
|
80
81
|
return build(VERDICT_ERRORED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
81
82
|
}
|
|
83
|
+
// #2672: capacity stall is distinct from ordinary not_ready_yet — surface
|
|
84
|
+
// immediately (exit 2) so agents wait for auto-failover instead of --skip-ci.
|
|
85
|
+
if (probe.ciReadyState === "runner_capacity_stall") {
|
|
86
|
+
return build(VERDICT_RUNNER_CAPACITY_STALL, EXIT_TERMINAL_ERROR, probe, poll);
|
|
87
|
+
}
|
|
88
|
+
// #2688: Greptile side satisfied on HEAD but CI red — fail loud toward a
|
|
89
|
+
// fix loop instead of burning max-wait-minutes on idle Greptile polls.
|
|
90
|
+
if (probe.cleanGateHoldout === "ci_failures") {
|
|
91
|
+
ciBlockedStreak += 1;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
ciBlockedStreak = 0;
|
|
95
|
+
}
|
|
96
|
+
if (oneShot && probe.cleanGateHoldout === "ci_failures") {
|
|
97
|
+
return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
98
|
+
}
|
|
99
|
+
if (!oneShot && ciBlockedStreak >= DEFAULT_CI_BLOCKED_THRESHOLD) {
|
|
100
|
+
return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
101
|
+
}
|
|
82
102
|
// Stall = a review IS present but stuck on a non-HEAD commit; surface it
|
|
83
103
|
// rather than burning the whole cap waiting for a re-review that isn't coming.
|
|
84
104
|
if (probe.found && !probe.shaMatch) {
|
|
@@ -112,6 +132,9 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
112
132
|
hasBlocking: false,
|
|
113
133
|
errored: false,
|
|
114
134
|
ciFailures: 0,
|
|
135
|
+
ciFailedChecks: [],
|
|
136
|
+
ciReadyState: null,
|
|
137
|
+
ciCapacityStalledChecks: [],
|
|
115
138
|
terminalCheckRun: false,
|
|
116
139
|
isClean: false,
|
|
117
140
|
cleanGateHoldout: null,
|
package/dist/scope/main.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
|
|
3
4
|
import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
|
|
4
5
|
import { canonicalLogPath, readAll } from "./audit-log.js";
|
|
5
6
|
import { TRANSITIONS } from "./constants.js";
|
|
@@ -110,6 +111,17 @@ export function lifecycleMain(argv) {
|
|
|
110
111
|
catch {
|
|
111
112
|
/* best-effort drift warning + reconcile; lifecycle success remains authoritative */
|
|
112
113
|
}
|
|
114
|
+
try {
|
|
115
|
+
const rootForTickler = resolveProjectRoot(projectRoot) ??
|
|
116
|
+
dirname(dirname(dirname(completedPathForScopeMove(filePath))));
|
|
117
|
+
const tickler = maybeRunStalenessTickler(rootForTickler);
|
|
118
|
+
for (const line of tickler.lines) {
|
|
119
|
+
process.stdout.write(`${line}\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* best-effort staleness tickler; lifecycle success remains authoritative */
|
|
124
|
+
}
|
|
113
125
|
}
|
|
114
126
|
return 0;
|
|
115
127
|
}
|
package/dist/scope/undo.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
3
4
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
4
5
|
import { append, canonicalLogPath, newDecisionId, readAll } from "./audit-log.js";
|
|
5
6
|
import { REVERSIBLE_ACTIONS, TERMINAL_ACTIONS } from "./constants.js";
|
|
@@ -157,7 +158,7 @@ function inversePlan(entry, logEntries) {
|
|
|
157
158
|
}
|
|
158
159
|
return null;
|
|
159
160
|
}
|
|
160
|
-
function moveAndFlip(srcFile, destFolder, newStatus, timestamp) {
|
|
161
|
+
function moveAndFlip(srcFile, destFolder, newStatus, timestamp, projectRoot) {
|
|
161
162
|
if (!existsSync(srcFile)) {
|
|
162
163
|
return [false, `File not found: ${srcFile}`, null];
|
|
163
164
|
}
|
|
@@ -172,6 +173,15 @@ function moveAndFlip(srcFile, destFolder, newStatus, timestamp) {
|
|
|
172
173
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
173
174
|
return [false, `Missing or invalid 'plan' object in ${srcFile}`, null];
|
|
174
175
|
}
|
|
176
|
+
try {
|
|
177
|
+
assertWriteTargetSafe(projectRoot, srcFile);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err instanceof ProjectionContainmentError) {
|
|
181
|
+
return [false, err.message, null];
|
|
182
|
+
}
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
175
185
|
const planObj = plan;
|
|
176
186
|
planObj.status = newStatus;
|
|
177
187
|
planObj.updated = timestamp;
|
|
@@ -262,7 +272,7 @@ export function undoOne(entry, projectRoot, options = {}) {
|
|
|
262
272
|
});
|
|
263
273
|
return { ok: true, message: msg, auditEntry: preview };
|
|
264
274
|
}
|
|
265
|
-
const [ok, fsMsg, destPath] = moveAndFlip(srcPath, destFolderPath, plan.newStatus, timestamp);
|
|
275
|
+
const [ok, fsMsg, destPath] = moveAndFlip(srcPath, destFolderPath, plan.newStatus, timestamp, projectRoot);
|
|
266
276
|
if (!ok || destPath === null) {
|
|
267
277
|
return { ok: false, message: fsMsg, auditEntry: null };
|
|
268
278
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ReleaseAvailabilityProbeOptions {
|
|
2
|
+
readonly now?: Date;
|
|
3
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
4
|
+
readonly readText?: (path: string) => string | null;
|
|
5
|
+
readonly isFile?: (path: string) => boolean;
|
|
6
|
+
readonly runNpmView?: () => {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
version: string;
|
|
9
|
+
};
|
|
10
|
+
readonly readState?: (path: string) => string | null;
|
|
11
|
+
readonly writeState?: (path: string, content: string) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ReleaseAvailabilityProbeResult {
|
|
14
|
+
readonly lines: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Probe the public npm registry separately from doctor. Doctor stays offline
|
|
18
|
+
* by default (#2182); mutable session start may emit this bounded advisory.
|
|
19
|
+
*/
|
|
20
|
+
export declare function probeSessionReleaseAvailability(projectRoot: string, options?: ReleaseAvailabilityProbeOptions): ReleaseAvailabilityProbeResult;
|
|
21
|
+
//# sourceMappingURL=release-availability.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
|
|
5
|
+
import { runningInsideDeftRepo } from "../doctor/paths.js";
|
|
6
|
+
import { evaluateReleaseAvailability } from "../doctor/release-availability.js";
|
|
7
|
+
const THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
9
|
+
const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "release-availability-state.json");
|
|
10
|
+
function defaultReadText(path) {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(path, "utf8");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function defaultNpmView() {
|
|
19
|
+
const result = spawnSync("npm", [
|
|
20
|
+
"view",
|
|
21
|
+
"@deftai/directive",
|
|
22
|
+
"version",
|
|
23
|
+
`--registry=${PUBLIC_NPM_REGISTRY}`,
|
|
24
|
+
"--ignore-scripts",
|
|
25
|
+
], {
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
});
|
|
29
|
+
const version = (result.stdout ?? "").trim().split(/\r?\n/)[0]?.trim() ?? "";
|
|
30
|
+
return { ok: result.status === 0 && version.length > 0, version };
|
|
31
|
+
}
|
|
32
|
+
function parseState(text) {
|
|
33
|
+
if (text === null)
|
|
34
|
+
return {};
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(text);
|
|
37
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// A stale advisory state is never allowed to block session start.
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
function isThrottled(state, latestVersion, now) {
|
|
47
|
+
if (state.latestVersion !== latestVersion || !state.notifiedAt)
|
|
48
|
+
return false;
|
|
49
|
+
const notifiedAt = Date.parse(state.notifiedAt);
|
|
50
|
+
return Number.isFinite(notifiedAt) && now.getTime() - notifiedAt < THROTTLE_MS;
|
|
51
|
+
}
|
|
52
|
+
function defaultWriteState(path, content) {
|
|
53
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
54
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
55
|
+
writeFileSync(temporary, content, "utf8");
|
|
56
|
+
renameSync(temporary, path);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Probe the public npm registry separately from doctor. Doctor stays offline
|
|
60
|
+
* by default (#2182); mutable session start may emit this bounded advisory.
|
|
61
|
+
*/
|
|
62
|
+
export function probeSessionReleaseAvailability(projectRoot, options = {}) {
|
|
63
|
+
const env = options.env ?? process.env;
|
|
64
|
+
if (env.DEFT_NO_NETWORK === "1") {
|
|
65
|
+
return { lines: ["[deft release] skipped (DEFT_NO_NETWORK=1)."] };
|
|
66
|
+
}
|
|
67
|
+
if (runningInsideDeftRepo(projectRoot)) {
|
|
68
|
+
return { lines: [] };
|
|
69
|
+
}
|
|
70
|
+
const readText = options.readText ?? defaultReadText;
|
|
71
|
+
const isFile = options.isFile ?? existsSync;
|
|
72
|
+
const manifestPath = locateManifest(projectRoot, null, isFile);
|
|
73
|
+
const manifestText = manifestPath ? readText(manifestPath) : null;
|
|
74
|
+
if (manifestText === null)
|
|
75
|
+
return { lines: [] };
|
|
76
|
+
const manifest = parseInstallManifest(manifestText);
|
|
77
|
+
const installed = (manifest.tag ?? manifest.ref ?? "").trim();
|
|
78
|
+
if (evaluateReleaseAvailability(installed, null).status === "not-applicable") {
|
|
79
|
+
return { lines: [] };
|
|
80
|
+
}
|
|
81
|
+
const lines = [
|
|
82
|
+
`[deft release] Checking ${PUBLIC_NPM_REGISTRY} for a newer published Directive release.`,
|
|
83
|
+
];
|
|
84
|
+
let npmResult;
|
|
85
|
+
try {
|
|
86
|
+
npmResult = (options.runNpmView ?? defaultNpmView)();
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
npmResult = { ok: false, version: "" };
|
|
90
|
+
}
|
|
91
|
+
const availability = evaluateReleaseAvailability(installed, npmResult.ok ? npmResult.version : null);
|
|
92
|
+
if (availability.status !== "available")
|
|
93
|
+
return { lines };
|
|
94
|
+
const statePath = join(projectRoot, STATE_RELATIVE_PATH);
|
|
95
|
+
const state = parseState((options.readState ?? defaultReadText)(statePath));
|
|
96
|
+
const now = options.now ?? new Date();
|
|
97
|
+
if (isThrottled(state, availability.latestVersion, now))
|
|
98
|
+
return { lines: [] };
|
|
99
|
+
const message = `[deft release] Newer Directive release available: v${availability.latestVersion} ` +
|
|
100
|
+
`(installed v${availability.installedVersion}). Run \`npm i -g @deftai/directive@latest\`.`;
|
|
101
|
+
try {
|
|
102
|
+
(options.writeState ?? defaultWriteState)(statePath, `${JSON.stringify({ latestVersion: availability.latestVersion, notifiedAt: now.toISOString() }, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// The advisory remains useful if its best-effort throttle state cannot persist.
|
|
106
|
+
}
|
|
107
|
+
return { lines: [...lines, message] };
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=release-availability.js.map
|
|
@@ -43,6 +43,20 @@ export declare function newRitualStatePayload(input: {
|
|
|
43
43
|
}): Record<string, unknown>;
|
|
44
44
|
export declare function readRitualState(projectRoot: string): [RitualState | null, string | null];
|
|
45
45
|
export declare function writeRitualState(projectRoot: string, payload: Record<string, unknown>): string;
|
|
46
|
+
/** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
|
|
47
|
+
export declare const RITUAL_STALE_EPOCH: Date;
|
|
48
|
+
export interface MarkRitualStaleAfterCompactResult {
|
|
49
|
+
readonly changed: boolean;
|
|
50
|
+
readonly statePath: string;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Invalidate an existing mutation ritual after host context compaction/resume (#2113).
|
|
55
|
+
* Reuses ritual-state age semantics — no parallel policy stack.
|
|
56
|
+
*/
|
|
57
|
+
export declare function markRitualStaleAfterCompact(projectRoot: string, input?: {
|
|
58
|
+
now?: Date;
|
|
59
|
+
}): MarkRitualStaleAfterCompactResult;
|
|
46
60
|
export declare function recordRitualStep(projectRoot: string, input: {
|
|
47
61
|
tier: "quick" | "gated";
|
|
48
62
|
stepName: string;
|
|
@@ -184,6 +184,34 @@ export function writeRitualState(projectRoot, payload) {
|
|
|
184
184
|
atomicWriteJson(stateFile, payload, ".ritual-state.");
|
|
185
185
|
return stateFile;
|
|
186
186
|
}
|
|
187
|
+
/** Instant guaranteed to fail `evaluateLoadedState` age checks on any policy horizon. */
|
|
188
|
+
export const RITUAL_STALE_EPOCH = new Date("1970-01-01T00:00:00Z");
|
|
189
|
+
/**
|
|
190
|
+
* Invalidate an existing mutation ritual after host context compaction/resume (#2113).
|
|
191
|
+
* Reuses ritual-state age semantics — no parallel policy stack.
|
|
192
|
+
*/
|
|
193
|
+
export function markRitualStaleAfterCompact(projectRoot, input = {}) {
|
|
194
|
+
const now = input.now ?? new Date();
|
|
195
|
+
const statePath = ritualStatePath(projectRoot);
|
|
196
|
+
const [state, err] = readRitualState(projectRoot);
|
|
197
|
+
if (state === null) {
|
|
198
|
+
return {
|
|
199
|
+
changed: false,
|
|
200
|
+
statePath,
|
|
201
|
+
message: err ?? "no ritual state to invalidate after compaction",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const payload = { ...state.raw };
|
|
205
|
+
payload.started_at = timestampIso(RITUAL_STALE_EPOCH);
|
|
206
|
+
payload.compact_resume_at = timestampIso(now);
|
|
207
|
+
writeRitualState(projectRoot, payload);
|
|
208
|
+
return {
|
|
209
|
+
changed: true,
|
|
210
|
+
statePath,
|
|
211
|
+
message: "Marked session ritual stale after context compaction; run session:start and " +
|
|
212
|
+
"verify:session-ritual -- --tier=gated before direct writes.",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
187
215
|
export function recordRitualStep(projectRoot, input) {
|
|
188
216
|
const [state, err] = readRitualState(projectRoot);
|
|
189
217
|
if (state === null) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type EnvironmentContext } from "../platform/shell-context.js";
|
|
2
2
|
import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
|
|
3
3
|
import type { GitRunner } from "./git.js";
|
|
4
|
+
import { type ReleaseAvailabilityProbeOptions } from "./release-availability.js";
|
|
4
5
|
import { ritualStatePath } from "./ritual-sentinel.js";
|
|
5
6
|
export declare const SESSION_POSTURES: readonly ["read-only", "mutation"];
|
|
6
7
|
export type SessionPosture = (typeof SESSION_POSTURES)[number];
|
|
@@ -42,6 +43,15 @@ export interface SessionStartOptions {
|
|
|
42
43
|
};
|
|
43
44
|
readonly resolveUserMd?: (projectRoot: string) => ResolveUserMdResult;
|
|
44
45
|
readonly probeEnvironment?: () => EnvironmentContext;
|
|
46
|
+
readonly probeReleaseAvailability?: (projectRoot: string, options: ReleaseAvailabilityProbeOptions) => {
|
|
47
|
+
lines: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
readonly runStalenessTickler?: (projectRoot: string, options: {
|
|
50
|
+
now?: Date;
|
|
51
|
+
}) => {
|
|
52
|
+
lines: readonly string[];
|
|
53
|
+
prompted: boolean;
|
|
54
|
+
};
|
|
45
55
|
}
|
|
46
56
|
export declare function parseDeferrals(rawValues: readonly string[]): {
|
|
47
57
|
deferrals: Record<string, string>;
|
|
@@ -5,11 +5,13 @@ 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 { resolvePolicy } from "../policy/resolve.js";
|
|
8
|
+
import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
|
|
8
9
|
import { runDefaultMode } from "../triage/welcome/default-mode.js";
|
|
9
10
|
import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
|
|
10
11
|
import { emitSessionValueReadback } from "../value/readback.js";
|
|
11
12
|
import { verifyRequiredTools } from "../verify-env/verify-tools.js";
|
|
12
13
|
import { defaultGitRunner, gitHead, worktreePath } from "./git.js";
|
|
14
|
+
import { probeSessionReleaseAvailability, } from "./release-availability.js";
|
|
13
15
|
import { newRitualStatePayload, ritualStatePath, ritualStep, writeRitualState, } from "./ritual-sentinel.js";
|
|
14
16
|
export const SESSION_POSTURES = ["read-only", "mutation"];
|
|
15
17
|
export const READ_ONLY_POSTURE = "read-only";
|
|
@@ -341,6 +343,22 @@ export function runSessionStart(projectRoot, options = {}) {
|
|
|
341
343
|
lines.push(message);
|
|
342
344
|
}
|
|
343
345
|
}
|
|
346
|
+
try {
|
|
347
|
+
const releaseAvailability = (options.probeReleaseAvailability ?? probeSessionReleaseAvailability)(projectRoot, { now: instant });
|
|
348
|
+
lines.push(...releaseAvailability.lines);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// Release availability is a best-effort operator advisory, never a session blocker.
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const tickler = (options.runStalenessTickler ?? maybeRunStalenessTickler)(projectRoot, {
|
|
355
|
+
now: instant,
|
|
356
|
+
});
|
|
357
|
+
lines.push(...tickler.lines);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Staleness tickler is best-effort and must never block session start.
|
|
361
|
+
}
|
|
344
362
|
if (!runningInsideDeftRepo(projectRoot) && shouldEmitMigrateNudge(projectRoot)) {
|
|
345
363
|
lines.push(MIGRATE_COMPLETION_NUDGE);
|
|
346
364
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DriftInputs, DriftScoreResult, StalenessTicklerPolicy, StalenessTicklerState, StalenessTicklerTier, XbriefSchemaDistance } from "./types.js";
|
|
2
|
+
/** Compute a weighted drift score from independent axes (#2489). */
|
|
3
|
+
export declare function scoreDrift(inputs: DriftInputs, policy: StalenessTicklerPolicy): number;
|
|
4
|
+
/** Map drift inputs to an escalation tier using typed thresholds. */
|
|
5
|
+
export declare function resolveTier(inputs: DriftInputs, policy: StalenessTicklerPolicy): StalenessTicklerTier;
|
|
6
|
+
/** Hold the last known tier when detection is unverified (#2489 monotonicity). */
|
|
7
|
+
export declare function holdTierOnUnverified(computedTier: StalenessTicklerTier, computedScore: number, state: StalenessTicklerState, directiveUnverified: boolean): DriftScoreResult;
|
|
8
|
+
/** Snooze window widens with deferrals up to `maxWidenMultiplier`. */
|
|
9
|
+
export declare function snoozeWindowMs(tier: StalenessTicklerTier, deferralCount: number, policy: StalenessTicklerPolicy): number;
|
|
10
|
+
export declare function isSnoozeActive(state: StalenessTicklerState, now: Date): boolean;
|
|
11
|
+
/** Assert tier re-prompts at idle even when a plain snooze would still be active. */
|
|
12
|
+
export declare function shouldPromptDespiteSnooze(tier: StalenessTicklerTier, state: StalenessTicklerState): boolean;
|
|
13
|
+
export declare function mergeHeldXbriefDistance(current: XbriefSchemaDistance, held: XbriefSchemaDistance | undefined): XbriefSchemaDistance;
|
|
14
|
+
//# sourceMappingURL=escalation.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
2
|
+
/** Compute a weighted drift score from independent axes (#2489). */
|
|
3
|
+
export function scoreDrift(inputs, policy) {
|
|
4
|
+
const { weights } = policy;
|
|
5
|
+
let score = 0;
|
|
6
|
+
if (inputs.directive.stale) {
|
|
7
|
+
if (inputs.directive.majorBehind) {
|
|
8
|
+
score += weights.directiveMajor;
|
|
9
|
+
}
|
|
10
|
+
score += inputs.directive.minorDistance * weights.directiveMinor;
|
|
11
|
+
score += inputs.directive.patchDistance * weights.directivePatch;
|
|
12
|
+
}
|
|
13
|
+
if (inputs.xbrief.stale) {
|
|
14
|
+
if (inputs.xbrief.distance === "behind-major") {
|
|
15
|
+
score += weights.schemaMajor;
|
|
16
|
+
}
|
|
17
|
+
else if (inputs.xbrief.distance === "behind-minor") {
|
|
18
|
+
score += weights.schemaMinor;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
score += (inputs.ageMs / MS_PER_DAY) * weights.agePerDay;
|
|
22
|
+
score += inputs.deferralCount * weights.deferral;
|
|
23
|
+
return score;
|
|
24
|
+
}
|
|
25
|
+
function baseTierFromInputs(inputs, policy) {
|
|
26
|
+
const { tiers } = policy;
|
|
27
|
+
if (inputs.directive.majorBehind ||
|
|
28
|
+
inputs.xbrief.distance === "behind-major" ||
|
|
29
|
+
inputs.ageMs >= tiers.strongAgeMs) {
|
|
30
|
+
return "strong";
|
|
31
|
+
}
|
|
32
|
+
if (inputs.directive.minorDistance >= tiers.noticeMinorThreshold ||
|
|
33
|
+
inputs.xbrief.distance === "behind-minor") {
|
|
34
|
+
return "notice";
|
|
35
|
+
}
|
|
36
|
+
return "quiet";
|
|
37
|
+
}
|
|
38
|
+
/** Map drift inputs to an escalation tier using typed thresholds. */
|
|
39
|
+
export function resolveTier(inputs, policy) {
|
|
40
|
+
const base = baseTierFromInputs(inputs, policy);
|
|
41
|
+
if (base === "strong" && inputs.deferralCount >= policy.tiers.assertDeferralCap) {
|
|
42
|
+
return "assert";
|
|
43
|
+
}
|
|
44
|
+
return base;
|
|
45
|
+
}
|
|
46
|
+
/** Hold the last known tier when detection is unverified (#2489 monotonicity). */
|
|
47
|
+
export function holdTierOnUnverified(computedTier, computedScore, state, directiveUnverified) {
|
|
48
|
+
if (!directiveUnverified || state.lastTier === undefined) {
|
|
49
|
+
return { score: computedScore, tier: computedTier };
|
|
50
|
+
}
|
|
51
|
+
const heldTier = state.lastTier;
|
|
52
|
+
const heldScore = state.lastScore ?? computedScore;
|
|
53
|
+
const tierRank = (tier) => tier === "quiet" ? 0 : tier === "notice" ? 1 : tier === "strong" ? 2 : 3;
|
|
54
|
+
if (tierRank(heldTier) > tierRank(computedTier)) {
|
|
55
|
+
return { score: heldScore, tier: heldTier };
|
|
56
|
+
}
|
|
57
|
+
return { score: computedScore, tier: computedTier };
|
|
58
|
+
}
|
|
59
|
+
function baseSnoozeMs(tier, policy) {
|
|
60
|
+
switch (tier) {
|
|
61
|
+
case "quiet":
|
|
62
|
+
return policy.snooze.quietMs;
|
|
63
|
+
case "notice":
|
|
64
|
+
return policy.snooze.noticeMs;
|
|
65
|
+
case "strong":
|
|
66
|
+
return policy.snooze.strongMs;
|
|
67
|
+
case "assert":
|
|
68
|
+
return policy.snooze.strongMs;
|
|
69
|
+
default:
|
|
70
|
+
return policy.snooze.noticeMs;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Snooze window widens with deferrals up to `maxWidenMultiplier`. */
|
|
74
|
+
export function snoozeWindowMs(tier, deferralCount, policy) {
|
|
75
|
+
const base = baseSnoozeMs(tier, policy);
|
|
76
|
+
const multiplier = Math.min(1 + deferralCount, Math.max(1, policy.snooze.maxWidenMultiplier));
|
|
77
|
+
if (tier === "assert") {
|
|
78
|
+
return Math.min(base, policy.snooze.strongMs);
|
|
79
|
+
}
|
|
80
|
+
return Math.round(base * multiplier);
|
|
81
|
+
}
|
|
82
|
+
export function isSnoozeActive(state, now) {
|
|
83
|
+
if (!state.snoozedUntil) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const until = Date.parse(state.snoozedUntil);
|
|
87
|
+
return Number.isFinite(until) && now.getTime() < until;
|
|
88
|
+
}
|
|
89
|
+
/** Assert tier re-prompts at idle even when a plain snooze would still be active. */
|
|
90
|
+
export function shouldPromptDespiteSnooze(tier, state) {
|
|
91
|
+
if (tier !== "assert") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (state.remindAfterNextRelease) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
export function mergeHeldXbriefDistance(current, held) {
|
|
100
|
+
if (held === undefined) {
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
const rank = (distance) => distance === "current" ? 0 : distance === "behind-minor" ? 1 : 2;
|
|
104
|
+
return rank(held) > rank(current) ? held : current;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=escalation.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StalenessTicklerPolicy } from "./types.js";
|
|
2
|
+
export interface IdleGateOptions {
|
|
3
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
4
|
+
readonly readPorcelain?: (projectRoot: string) => string | null;
|
|
5
|
+
readonly countInFlight?: (projectRoot: string) => number;
|
|
6
|
+
readonly insideDeftRepo?: (projectRoot: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface IdleGateResult {
|
|
9
|
+
readonly ok: boolean;
|
|
10
|
+
readonly reason?: string;
|
|
11
|
+
}
|
|
12
|
+
/** True when every #2488 idle gate passes for an interactive tickler prompt. */
|
|
13
|
+
export declare function isSafeIdlePoint(projectRoot: string, policy: StalenessTicklerPolicy, options?: IdleGateOptions): IdleGateResult;
|
|
14
|
+
/** Skip the tickler entirely in CI/headless ritual-skip contexts. */
|
|
15
|
+
export declare function shouldSkipTicklerEntirely(env?: NodeJS.ProcessEnv): boolean;
|
|
16
|
+
/** Whether stdin/stdout are TTYs suitable for a blocking consent prompt. */
|
|
17
|
+
export declare function isInteractiveSession(env?: NodeJS.ProcessEnv, isTty?: () => boolean): boolean;
|
|
18
|
+
//# sourceMappingURL=idle.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { runningInsideDeftRepo } from "../doctor/paths.js";
|
|
2
|
+
import { gitPorcelain } from "../story-ready/git.js";
|
|
3
|
+
import { countFilesystemInFlight } from "../triage/summary/index.js";
|
|
4
|
+
function ritualSkipActive(env) {
|
|
5
|
+
return env.DEFT_SESSION_RITUAL_SKIP === "1";
|
|
6
|
+
}
|
|
7
|
+
/** True when every #2488 idle gate passes for an interactive tickler prompt. */
|
|
8
|
+
export function isSafeIdlePoint(projectRoot, policy, options = {}) {
|
|
9
|
+
const env = options.env ?? process.env;
|
|
10
|
+
if (ritualSkipActive(env)) {
|
|
11
|
+
return { ok: false, reason: "DEFT_SESSION_RITUAL_SKIP=1" };
|
|
12
|
+
}
|
|
13
|
+
if (!policy.enabled || policy.optOut) {
|
|
14
|
+
return { ok: false, reason: "policy-disabled" };
|
|
15
|
+
}
|
|
16
|
+
const insideDeft = options.insideDeftRepo ?? ((root) => runningInsideDeftRepo(root));
|
|
17
|
+
if (insideDeft(projectRoot)) {
|
|
18
|
+
return { ok: false, reason: "framework-repo" };
|
|
19
|
+
}
|
|
20
|
+
const porcelain = (options.readPorcelain ?? gitPorcelain)(projectRoot);
|
|
21
|
+
if (porcelain === null) {
|
|
22
|
+
return { ok: false, reason: "git-undeterminable" };
|
|
23
|
+
}
|
|
24
|
+
if (porcelain.trim().length > 0) {
|
|
25
|
+
return { ok: false, reason: "dirty-tree" };
|
|
26
|
+
}
|
|
27
|
+
const inFlight = (options.countInFlight ?? countFilesystemInFlight)(projectRoot);
|
|
28
|
+
if (inFlight > 0) {
|
|
29
|
+
return { ok: false, reason: "story-in-flight" };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
/** Skip the tickler entirely in CI/headless ritual-skip contexts. */
|
|
34
|
+
export function shouldSkipTicklerEntirely(env = process.env) {
|
|
35
|
+
return ritualSkipActive(env);
|
|
36
|
+
}
|
|
37
|
+
/** Whether stdin/stdout are TTYs suitable for a blocking consent prompt. */
|
|
38
|
+
export function isInteractiveSession(env = process.env, isTty = () => process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
39
|
+
if (env.DEFT_SESSION_RITUAL_SKIP === "1") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (env.CI === "true" || env.CI === "1") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return isTty();
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=idle.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./escalation.js";
|
|
2
|
+
export * from "./idle.js";
|
|
3
|
+
export * from "./probe-directive.js";
|
|
4
|
+
export * from "./probe-xbrief.js";
|
|
5
|
+
export { DIRECTIVE_UPGRADE_COMMAND, maybeRunStalenessTickler, probeStalenessDimensions, XBRIEF_MIGRATE_COMMAND, } from "./run.js";
|
|
6
|
+
export * from "./state.js";
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./escalation.js";
|
|
2
|
+
export * from "./idle.js";
|
|
3
|
+
export * from "./probe-directive.js";
|
|
4
|
+
export * from "./probe-xbrief.js";
|
|
5
|
+
export { DIRECTIVE_UPGRADE_COMMAND, maybeRunStalenessTickler, probeStalenessDimensions, XBRIEF_MIGRATE_COMMAND, } from "./run.js";
|
|
6
|
+
export * from "./state.js";
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReleaseAvailabilityResult } from "../doctor/release-availability.js";
|
|
2
|
+
import type { ParsedSemverCore } from "./types.js";
|
|
3
|
+
export interface ProbeDirectiveOptions {
|
|
4
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
5
|
+
readonly readText?: (path: string) => string | null;
|
|
6
|
+
readonly isFile?: (path: string) => boolean;
|
|
7
|
+
readonly runNpmView?: () => {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** Parse `X.Y.Z` or `vX.Y.Z` core semver (ignores pre-release suffix). */
|
|
13
|
+
export declare function parseSemverCore(version: string): ParsedSemverCore | null;
|
|
14
|
+
export declare function computeDirectiveDistance(installedVersion: string, latestVersion: string): {
|
|
15
|
+
majorBehind: boolean;
|
|
16
|
+
minorDistance: number;
|
|
17
|
+
patchDistance: number;
|
|
18
|
+
};
|
|
19
|
+
export interface ProbeDirectiveResult {
|
|
20
|
+
readonly availability: ReleaseAvailabilityResult;
|
|
21
|
+
readonly majorBehind: boolean;
|
|
22
|
+
readonly minorDistance: number;
|
|
23
|
+
readonly patchDistance: number;
|
|
24
|
+
readonly stale: boolean;
|
|
25
|
+
readonly registryDisclosure: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Probe Directive payload staleness via the public npm registry (#1692 substrate).
|
|
29
|
+
* Skips network when `DEFT_NO_NETWORK=1`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function probeDirectiveStaleness(projectRoot: string, options?: ProbeDirectiveOptions): ProbeDirectiveResult | null;
|
|
32
|
+
//# sourceMappingURL=probe-directive.d.ts.map
|