@deftai/directive-core 0.94.0 → 0.96.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/authz/classify.js +485 -0
- package/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/doctor/main.js +41 -9
- package/dist/doctor/types.d.ts +2 -2
- package/dist/freshness/bind.d.ts +67 -0
- package/dist/freshness/bind.js +201 -0
- package/dist/freshness/cli.d.ts +30 -0
- package/dist/freshness/cli.js +180 -0
- package/dist/freshness/compare.d.ts +14 -0
- package/dist/freshness/compare.js +134 -0
- package/dist/freshness/generation.d.ts +44 -0
- package/dist/freshness/generation.js +172 -0
- package/dist/freshness/index.d.ts +13 -0
- package/dist/freshness/index.js +13 -0
- package/dist/freshness/report.d.ts +43 -0
- package/dist/freshness/report.js +139 -0
- package/dist/freshness/types.d.ts +70 -0
- package/dist/freshness/types.js +30 -0
- package/dist/handoff-evidence/index.d.ts +8 -0
- package/dist/handoff-evidence/index.js +7 -0
- package/dist/handoff-evidence/validate.d.ts +105 -0
- package/dist/handoff-evidence/validate.js +423 -0
- package/dist/hooks/dispatcher.d.ts +3 -0
- package/dist/hooks/dispatcher.js +12 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +5 -2
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- package/dist/init-deposit/init-deposit.d.ts +7 -1
- package/dist/init-deposit/init-deposit.js +24 -4
- package/dist/init-deposit/refresh.d.ts +9 -1
- package/dist/init-deposit/refresh.js +48 -5
- package/dist/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +35 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -0
- package/dist/session/ritual-entrypoint.d.ts +6 -2
- package/dist/session/ritual-entrypoint.js +15 -2
- package/dist/session/session-ready.js +31 -14
- package/dist/session/session-start.d.ts +2 -1
- package/dist/session/session-start.js +66 -3
- package/dist/session/verify-session-ritual.d.ts +4 -2
- package/dist/session/verify-session-ritual.js +11 -11
- package/dist/slash/product-set.js +4 -1
- package/dist/swarm/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/swarm/verify-review-clean.d.ts +4 -3
- package/dist/swarm/verify-review-clean.js +28 -73
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -0
- package/dist/triage/classify/index.d.ts +9 -0
- package/dist/triage/classify/index.js +34 -2
- package/dist/triage/classify/label-mirror.d.ts +206 -0
- package/dist/triage/classify/label-mirror.js +914 -0
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +134 -39
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- package/dist/triage/queue/show.d.ts +1 -1
- package/dist/triage/queue/show.js +4 -2
- package/dist/vbrief-validate/project-definition.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
- package/dist/verify-env/agent-hook-readiness.js +150 -0
- package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
- package/dist/verify-env/agent-hooks-live-probe.js +120 -107
- package/dist/verify-env/agent-hooks.js +11 -3
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/package.json +11 -3
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session bind of live deposit generation (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Host-agnostic storage:
|
|
5
|
+
* - Default (no sessionId): `.deft/session-bind.json` — single-operator convenience.
|
|
6
|
+
* - With sessionId: `.deft/session-binds/<safeId>.json` — isolated multi-session binds.
|
|
7
|
+
*
|
|
8
|
+
* Multi-agent hosts MUST pass a stable host session identity when binding and
|
|
9
|
+
* reporting so one session cannot certify another as current.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
14
|
+
import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
|
|
15
|
+
import { readLiveGeneration, stampLiveGeneration } from "./generation.js";
|
|
16
|
+
import { FRESHNESS_SCHEMA_VERSION, } from "./types.js";
|
|
17
|
+
/** Relative path for the default (no-sessionId) bind record. */
|
|
18
|
+
export const SESSION_BIND_REL = join(".deft", "session-bind.json");
|
|
19
|
+
/** Directory for per-session bind records. */
|
|
20
|
+
export const SESSION_BINDS_DIR_REL = join(".deft", "session-binds");
|
|
21
|
+
export function sessionBindPath(projectRoot, sessionId) {
|
|
22
|
+
const root = resolve(projectRoot);
|
|
23
|
+
const id = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
24
|
+
if (id.length === 0) {
|
|
25
|
+
return join(root, SESSION_BIND_REL);
|
|
26
|
+
}
|
|
27
|
+
return join(root, SESSION_BINDS_DIR_REL, safeSessionFileName(id));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Stable filesystem-safe file name for a host session id.
|
|
31
|
+
* Keeps a short prefix for debug, hashes the rest to avoid path injection.
|
|
32
|
+
* Character filter is O(n) (no regex) — CodeQL poly-redos on uncontrolled ids.
|
|
33
|
+
*/
|
|
34
|
+
export function safeSessionFileName(sessionId) {
|
|
35
|
+
const trimmed = sessionId.trim();
|
|
36
|
+
const hash = createHash("sha256").update(trimmed, "utf8").digest("hex").slice(0, 24);
|
|
37
|
+
let prefix = "";
|
|
38
|
+
for (let i = 0; i < trimmed.length && prefix.length < 32; i++) {
|
|
39
|
+
const ch = trimmed[i] ?? "";
|
|
40
|
+
const code = ch.charCodeAt(0);
|
|
41
|
+
const ok = (code >= 48 && code <= 57) || // 0-9
|
|
42
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
43
|
+
(code >= 97 && code <= 122) || // a-z
|
|
44
|
+
ch === "." ||
|
|
45
|
+
ch === "_" ||
|
|
46
|
+
ch === "-";
|
|
47
|
+
if (ok) {
|
|
48
|
+
prefix += ch;
|
|
49
|
+
}
|
|
50
|
+
else if (prefix.length === 0 || prefix[prefix.length - 1] !== "_") {
|
|
51
|
+
prefix += "_";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
while (prefix.startsWith("_")) {
|
|
55
|
+
prefix = prefix.slice(1);
|
|
56
|
+
}
|
|
57
|
+
while (prefix.endsWith("_")) {
|
|
58
|
+
prefix = prefix.slice(0, -1);
|
|
59
|
+
}
|
|
60
|
+
const base = prefix.length > 0 ? `${prefix}-${hash}` : hash;
|
|
61
|
+
return `${base}.json`;
|
|
62
|
+
}
|
|
63
|
+
function nowIsoDefault() {
|
|
64
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
65
|
+
}
|
|
66
|
+
/** Parse a bound generation record (null if invalid). */
|
|
67
|
+
export function parseBoundGeneration(raw) {
|
|
68
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const rec = raw;
|
|
72
|
+
const boundGeneration = rec.boundGeneration;
|
|
73
|
+
if (typeof boundGeneration !== "number" ||
|
|
74
|
+
!Number.isInteger(boundGeneration) ||
|
|
75
|
+
boundGeneration < 1) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const boundAt = typeof rec.boundAt === "string" && rec.boundAt.trim() ? rec.boundAt.trim() : null;
|
|
79
|
+
if (boundAt === null) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const contentVersion = typeof rec.contentVersion === "string" && rec.contentVersion.trim()
|
|
83
|
+
? rec.contentVersion.trim()
|
|
84
|
+
: "";
|
|
85
|
+
let surfaces = {};
|
|
86
|
+
if (typeof rec.surfaces === "object" && rec.surfaces !== null && !Array.isArray(rec.surfaces)) {
|
|
87
|
+
const next = {};
|
|
88
|
+
for (const [k, v] of Object.entries(rec.surfaces)) {
|
|
89
|
+
if (typeof v === "string") {
|
|
90
|
+
next[k] = v;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
surfaces = next;
|
|
94
|
+
}
|
|
95
|
+
const sessionId = typeof rec.sessionId === "string" && rec.sessionId.trim()
|
|
96
|
+
? rec.sessionId.trim()
|
|
97
|
+
: rec.sessionId === null
|
|
98
|
+
? null
|
|
99
|
+
: undefined;
|
|
100
|
+
const payloadLoaded = rec.payloadLoaded === true;
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: FRESHNESS_SCHEMA_VERSION,
|
|
103
|
+
boundGeneration,
|
|
104
|
+
boundAt,
|
|
105
|
+
contentVersion,
|
|
106
|
+
surfaces,
|
|
107
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
108
|
+
...(payloadLoaded ? { payloadLoaded: true } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Read the session bind record (null when absent/unreadable). */
|
|
112
|
+
export function readBoundGeneration(projectRoot, options = {}) {
|
|
113
|
+
const path = sessionBindPath(projectRoot, options.sessionId);
|
|
114
|
+
if (!existsSync(path)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const text = readFileSync(path, "utf8");
|
|
119
|
+
const bound = parseBoundGeneration(JSON.parse(text));
|
|
120
|
+
if (bound === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// When reading a session-scoped bind, refuse a record whose embedded
|
|
124
|
+
// sessionId disagrees (tamper / wrong file).
|
|
125
|
+
const want = typeof options.sessionId === "string" ? options.sessionId.trim() : "";
|
|
126
|
+
if (want.length > 0 && bound.sessionId && bound.sessionId !== want) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return bound;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function writeBoundAt(projectRoot, bound, sessionId) {
|
|
136
|
+
const root = resolve(projectRoot);
|
|
137
|
+
const target = sessionBindPath(projectRoot, sessionId);
|
|
138
|
+
const body = `${JSON.stringify(bound, null, 2)}\n`;
|
|
139
|
+
try {
|
|
140
|
+
containedWrite({
|
|
141
|
+
root,
|
|
142
|
+
target,
|
|
143
|
+
data: body,
|
|
144
|
+
mode: "replace",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
if (err instanceof ContainedWriteError) {
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
return target;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Bind the current live generation into session context.
|
|
157
|
+
*
|
|
158
|
+
* Does not require restarting a shared host runtime — callers re-load payload
|
|
159
|
+
* surfaces into the session and call this (or `freshness:bind`) to rebind.
|
|
160
|
+
*
|
|
161
|
+
* Multi-agent hosts MUST supply `sessionId` so binds do not overwrite each other.
|
|
162
|
+
*/
|
|
163
|
+
export function bindSessionGeneration(projectRoot, options = {}) {
|
|
164
|
+
let live = readLiveGeneration(projectRoot);
|
|
165
|
+
if (live === null && options.ensureLive !== false) {
|
|
166
|
+
const contentVersion = options.contentVersion?.trim() || "0.0.0";
|
|
167
|
+
live = stampLiveGeneration(projectRoot, {
|
|
168
|
+
contentVersion,
|
|
169
|
+
stampedBy: options.stampedBy ?? "session-bind",
|
|
170
|
+
increment: false,
|
|
171
|
+
nowIso: options.nowIso,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (live === null) {
|
|
175
|
+
throw new Error("freshness bind: no live generation token on disk; run directive update or init first");
|
|
176
|
+
}
|
|
177
|
+
const sessionId = typeof options.sessionId === "string" && options.sessionId.trim()
|
|
178
|
+
? options.sessionId.trim()
|
|
179
|
+
: options.sessionId === null
|
|
180
|
+
? null
|
|
181
|
+
: undefined;
|
|
182
|
+
const bound = {
|
|
183
|
+
schemaVersion: FRESHNESS_SCHEMA_VERSION,
|
|
184
|
+
boundGeneration: live.generation,
|
|
185
|
+
boundAt: options.nowIso ?? nowIsoDefault(),
|
|
186
|
+
contentVersion: live.contentVersion,
|
|
187
|
+
surfaces: { ...live.surfaces },
|
|
188
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
189
|
+
...(options.payloadLoaded === true ? { payloadLoaded: true } : {}),
|
|
190
|
+
};
|
|
191
|
+
const path = writeBoundAt(projectRoot, bound, sessionId ?? null);
|
|
192
|
+
// Opt-in only: never mirror session-scoped binds into the default path by
|
|
193
|
+
// default (that would let bare freshness:report certify another session).
|
|
194
|
+
if (sessionId &&
|
|
195
|
+
options.alsoWriteDefault === true &&
|
|
196
|
+
path !== sessionBindPath(projectRoot, null)) {
|
|
197
|
+
writeBoundAt(projectRoot, bound, null);
|
|
198
|
+
}
|
|
199
|
+
return { bound, live, path };
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=bind.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry for freshness:report / freshness:bind (#3117).
|
|
3
|
+
*/
|
|
4
|
+
export type FreshnessCliCommand = "report" | "bind" | "help";
|
|
5
|
+
export interface FreshnessCliOptions {
|
|
6
|
+
readonly command: FreshnessCliCommand;
|
|
7
|
+
readonly projectRoot: string | null;
|
|
8
|
+
readonly json: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Host session id for bind/report.
|
|
11
|
+
* - `undefined` (flag omitted): report recovers ritual session_id
|
|
12
|
+
* - string: explicit session bind
|
|
13
|
+
* - `""` empty flag value: force default bind path (no ritual recovery)
|
|
14
|
+
*/
|
|
15
|
+
readonly sessionId: string | undefined;
|
|
16
|
+
/** Host attests payload surfaces were reloaded before bind. */
|
|
17
|
+
readonly confirmPayloadLoaded: boolean;
|
|
18
|
+
readonly help: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface FreshnessCliResult {
|
|
21
|
+
readonly exitCode: number;
|
|
22
|
+
readonly stdout: string;
|
|
23
|
+
readonly stderr: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function parseFreshnessArgv(argv: readonly string[]): FreshnessCliOptions;
|
|
26
|
+
/** Run freshness CLI. Returns structured stdout/stderr + exit code. */
|
|
27
|
+
export declare function runFreshnessCli(argv?: readonly string[]): FreshnessCliResult;
|
|
28
|
+
/** Core-module main entry for dispatch (stdout/stderr + process.exit). */
|
|
29
|
+
export declare function mainEntry(argv?: string[]): number;
|
|
30
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry for freshness:report / freshness:bind (#3117).
|
|
3
|
+
*/
|
|
4
|
+
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
5
|
+
import { bindSessionGeneration } from "./bind.js";
|
|
6
|
+
import { formatFreshnessReport, freshnessReportExitCode, freshnessReportToJson, reportFreshness, resolveReportSessionId, } from "./report.js";
|
|
7
|
+
const HELP_TEXT = `Usage: deft freshness:<command> [options]
|
|
8
|
+
|
|
9
|
+
Host-agnostic freshness contract: compare session bound generation vs live deposit.
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
report Compare bound vs live (default). Exit 0=current, 1=soft/unbound, 2=hard.
|
|
13
|
+
bind Bind the current live generation into this session (rebind without host restart).
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--project-root <path> Project root (default: cwd / nearest project)
|
|
17
|
+
--session-id <id> Optional host session identity stored on bind
|
|
18
|
+
--confirm-payload-loaded Attest payload surfaces were reloaded (bind only)
|
|
19
|
+
--json Machine-readable JSON on stdout
|
|
20
|
+
--help Show this help
|
|
21
|
+
|
|
22
|
+
See content/docs/freshness-contract.md for soft vs hard meanings and mid-mission safety.
|
|
23
|
+
`;
|
|
24
|
+
export function parseFreshnessArgv(argv) {
|
|
25
|
+
let command = "report";
|
|
26
|
+
let projectRoot = null;
|
|
27
|
+
let json = false;
|
|
28
|
+
let sessionId;
|
|
29
|
+
let confirmPayloadLoaded = false;
|
|
30
|
+
let help = false;
|
|
31
|
+
const positional = [];
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const arg = argv[i] ?? "";
|
|
34
|
+
if (arg === "--help" || arg === "-h") {
|
|
35
|
+
help = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--json") {
|
|
39
|
+
json = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === "--confirm-payload-loaded" || arg === "--confirm-loaded") {
|
|
43
|
+
confirmPayloadLoaded = true;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (arg === "--project-root" || arg === "--cwd") {
|
|
47
|
+
projectRoot = argv[++i] ?? null;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith("--project-root=")) {
|
|
51
|
+
projectRoot = arg.slice("--project-root=".length) || null;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--session-id") {
|
|
55
|
+
// Empty value forces default bind; omitted flag stays undefined (ritual recover).
|
|
56
|
+
sessionId = argv[++i] ?? "";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith("--session-id=")) {
|
|
60
|
+
sessionId = arg.slice("--session-id=".length);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith("-")) {
|
|
64
|
+
// Unknown flag — leave for command validation via help path.
|
|
65
|
+
positional.push(arg);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
positional.push(arg);
|
|
69
|
+
}
|
|
70
|
+
if (positional[0] === "report" || positional[0] === "bind" || positional[0] === "help") {
|
|
71
|
+
command = positional[0];
|
|
72
|
+
}
|
|
73
|
+
else if (positional[0]) {
|
|
74
|
+
// Treat unknown subcommand as help-worthy.
|
|
75
|
+
help = true;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
command: help ? "help" : command,
|
|
79
|
+
projectRoot,
|
|
80
|
+
json,
|
|
81
|
+
sessionId,
|
|
82
|
+
confirmPayloadLoaded,
|
|
83
|
+
help,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Run freshness CLI. Returns structured stdout/stderr + exit code. */
|
|
87
|
+
export function runFreshnessCli(argv = []) {
|
|
88
|
+
const options = parseFreshnessArgv(argv);
|
|
89
|
+
if (options.help || options.command === "help") {
|
|
90
|
+
return { exitCode: 0, stdout: HELP_TEXT, stderr: "" };
|
|
91
|
+
}
|
|
92
|
+
const projectRoot = resolveProjectRoot(options.projectRoot);
|
|
93
|
+
if (projectRoot === null) {
|
|
94
|
+
return {
|
|
95
|
+
exitCode: 2,
|
|
96
|
+
stdout: "",
|
|
97
|
+
stderr: "freshness: could not resolve project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from a tree with xbrief/ or .git/.\n",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (options.command === "bind") {
|
|
101
|
+
try {
|
|
102
|
+
// Match report identity: omitted flag recovers ritual session_id so bare
|
|
103
|
+
// bind + bare report target the same scoped record (Greptile P1).
|
|
104
|
+
const bindSessionId = options.sessionId === undefined
|
|
105
|
+
? resolveReportSessionId(projectRoot, undefined)
|
|
106
|
+
: options.sessionId.trim().length > 0
|
|
107
|
+
? options.sessionId.trim()
|
|
108
|
+
: null;
|
|
109
|
+
const { bound, live, path } = bindSessionGeneration(projectRoot, {
|
|
110
|
+
sessionId: bindSessionId,
|
|
111
|
+
payloadLoaded: options.confirmPayloadLoaded,
|
|
112
|
+
});
|
|
113
|
+
if (options.json) {
|
|
114
|
+
return {
|
|
115
|
+
exitCode: 0,
|
|
116
|
+
stdout: `${JSON.stringify({
|
|
117
|
+
success: true,
|
|
118
|
+
action: "bind",
|
|
119
|
+
bound,
|
|
120
|
+
live,
|
|
121
|
+
path,
|
|
122
|
+
}, null, 2)}\n`,
|
|
123
|
+
stderr: "",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
exitCode: 0,
|
|
128
|
+
stdout: `[deft freshness] bound session to live generation ${live.generation} ` +
|
|
129
|
+
`(content v${live.contentVersion})\n` +
|
|
130
|
+
` bind path: ${path}\n` +
|
|
131
|
+
" Re-loaded payload surfaces are now the session's bound generation.\n",
|
|
132
|
+
stderr: "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
if (options.json) {
|
|
138
|
+
return {
|
|
139
|
+
exitCode: 2,
|
|
140
|
+
stdout: `${JSON.stringify({ success: false, action: "bind", error: message }, null, 2)}\n`,
|
|
141
|
+
stderr: "",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { exitCode: 2, stdout: "", stderr: `freshness:bind: ${message}\n` };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// report (default): omitted --session-id recovers ritual session_id;
|
|
148
|
+
// empty --session-id forces default bind; non-empty uses that id only.
|
|
149
|
+
const reportSessionId = options.sessionId === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: options.sessionId.trim().length > 0
|
|
152
|
+
? options.sessionId.trim()
|
|
153
|
+
: null;
|
|
154
|
+
const report = reportFreshness(projectRoot, { sessionId: reportSessionId });
|
|
155
|
+
const exitCode = freshnessReportExitCode(report);
|
|
156
|
+
if (options.json) {
|
|
157
|
+
return {
|
|
158
|
+
exitCode,
|
|
159
|
+
stdout: `${JSON.stringify(freshnessReportToJson(report), null, 2)}\n`,
|
|
160
|
+
stderr: "",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
exitCode,
|
|
165
|
+
stdout: formatFreshnessReport(report),
|
|
166
|
+
stderr: "",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Core-module main entry for dispatch (stdout/stderr + process.exit). */
|
|
170
|
+
export function mainEntry(argv = process.argv.slice(2)) {
|
|
171
|
+
const result = runFreshnessCli(argv);
|
|
172
|
+
if (result.stdout) {
|
|
173
|
+
process.stdout.write(result.stdout);
|
|
174
|
+
}
|
|
175
|
+
if (result.stderr) {
|
|
176
|
+
process.stderr.write(result.stderr);
|
|
177
|
+
}
|
|
178
|
+
return result.exitCode;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound vs live freshness comparison (#3117).
|
|
3
|
+
*/
|
|
4
|
+
import { type BoundGeneration, type FreshnessReport, type LiveGeneration } from "./types.js";
|
|
5
|
+
export declare const REBIND_GUIDANCE: string;
|
|
6
|
+
export declare const MID_MISSION_SAFETY: string;
|
|
7
|
+
export declare const UNBOUND_GUIDANCE: string;
|
|
8
|
+
/**
|
|
9
|
+
* Compare bound vs live generation and surface fingerprints.
|
|
10
|
+
*
|
|
11
|
+
* Ready only when state is `current` (bound matches live for used surfaces).
|
|
12
|
+
*/
|
|
13
|
+
export declare function compareFreshness(bound: BoundGeneration | null, live: LiveGeneration | null): FreshnessReport;
|
|
14
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound vs live freshness comparison (#3117).
|
|
3
|
+
*/
|
|
4
|
+
import { differingSurfaces } from "./generation.js";
|
|
5
|
+
import { HARD_SURFACES, SOFT_SURFACES, } from "./types.js";
|
|
6
|
+
export const REBIND_GUIDANCE = "Rebind without restarting the shared host runtime: re-load payload surfaces " +
|
|
7
|
+
"(skills, rituals, templates, commands) into the session context, then run " +
|
|
8
|
+
"`deft freshness:bind` (or `task freshness:bind`). Prefer `deft session:start --rearm` " +
|
|
9
|
+
"or `deft session:ready` when ritual state is still valid on this worktree.";
|
|
10
|
+
export const MID_MISSION_SAFETY = "Mid-mission safety: park and hand off in-flight work before a hard refresh/rebind. " +
|
|
11
|
+
"An empty session after refresh is not work complete — resume from the handoff, " +
|
|
12
|
+
"not from a blank context.";
|
|
13
|
+
export const UNBOUND_GUIDANCE = "No session bind recorded. Load payload surfaces into the session and run " +
|
|
14
|
+
"`deft freshness:bind` (session:start binds automatically on mutation posture). " +
|
|
15
|
+
"Disk-only version probes are insufficient for session readiness.";
|
|
16
|
+
function partitionDiffs(diffs) {
|
|
17
|
+
const hardSet = new Set(HARD_SURFACES);
|
|
18
|
+
const softSet = new Set(SOFT_SURFACES);
|
|
19
|
+
const hard = [];
|
|
20
|
+
const soft = [];
|
|
21
|
+
for (const s of diffs) {
|
|
22
|
+
if (hardSet.has(s)) {
|
|
23
|
+
hard.push(s);
|
|
24
|
+
}
|
|
25
|
+
else if (softSet.has(s)) {
|
|
26
|
+
soft.push(s);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Unknown surface: treat as hard (fail closed for trust).
|
|
30
|
+
hard.push(s);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { hard, soft };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compare bound vs live generation and surface fingerprints.
|
|
37
|
+
*
|
|
38
|
+
* Ready only when state is `current` (bound matches live for used surfaces).
|
|
39
|
+
*/
|
|
40
|
+
export function compareFreshness(bound, live) {
|
|
41
|
+
if (bound === null) {
|
|
42
|
+
return {
|
|
43
|
+
boundGeneration: null,
|
|
44
|
+
liveGeneration: live?.generation ?? null,
|
|
45
|
+
boundContentVersion: null,
|
|
46
|
+
liveContentVersion: live?.contentVersion ?? null,
|
|
47
|
+
state: "unbound",
|
|
48
|
+
differingSurfaces: [],
|
|
49
|
+
hardDiffs: [],
|
|
50
|
+
softDiffs: [],
|
|
51
|
+
ready: false,
|
|
52
|
+
rebindGuidance: UNBOUND_GUIDANCE,
|
|
53
|
+
midMissionSafety: MID_MISSION_SAFETY,
|
|
54
|
+
live,
|
|
55
|
+
bound: null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (live === null) {
|
|
59
|
+
return {
|
|
60
|
+
boundGeneration: bound.boundGeneration,
|
|
61
|
+
liveGeneration: null,
|
|
62
|
+
boundContentVersion: bound.contentVersion,
|
|
63
|
+
liveContentVersion: null,
|
|
64
|
+
state: "stale_hard",
|
|
65
|
+
differingSurfaces: ["payload", "version"],
|
|
66
|
+
hardDiffs: ["payload", "version"],
|
|
67
|
+
softDiffs: [],
|
|
68
|
+
ready: false,
|
|
69
|
+
rebindGuidance: "Live generation token missing. Run `directive update` / `deft update` to stamp " +
|
|
70
|
+
"the deposit, then rebind. " +
|
|
71
|
+
REBIND_GUIDANCE,
|
|
72
|
+
midMissionSafety: MID_MISSION_SAFETY,
|
|
73
|
+
live: null,
|
|
74
|
+
bound,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const diffs = differingSurfaces(bound.surfaces, live.surfaces);
|
|
78
|
+
const { hard, soft } = partitionDiffs(diffs);
|
|
79
|
+
const generationMismatch = bound.boundGeneration !== live.generation;
|
|
80
|
+
let state;
|
|
81
|
+
if (!generationMismatch && diffs.length === 0) {
|
|
82
|
+
state = "current";
|
|
83
|
+
}
|
|
84
|
+
else if (hard.length > 0 || generationMismatch) {
|
|
85
|
+
// Generation advance without hard surface proof still means payload re-apply.
|
|
86
|
+
if (generationMismatch &&
|
|
87
|
+
hard.length === 0 &&
|
|
88
|
+
soft.length > 0 &&
|
|
89
|
+
diffs.length === soft.length) {
|
|
90
|
+
state = "stale_soft";
|
|
91
|
+
}
|
|
92
|
+
else if (generationMismatch && hard.length === 0 && diffs.length === 0) {
|
|
93
|
+
// Generation bumped with identical fingerprints (rare) — treat as hard.
|
|
94
|
+
state = "stale_hard";
|
|
95
|
+
}
|
|
96
|
+
else if (hard.length > 0) {
|
|
97
|
+
state = "stale_hard";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
state = "stale_hard";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
state = "stale_soft";
|
|
105
|
+
}
|
|
106
|
+
// Clarify generation-only mismatch with no surface info on bound: hard.
|
|
107
|
+
if (generationMismatch && Object.keys(bound.surfaces).length === 0) {
|
|
108
|
+
state = "stale_hard";
|
|
109
|
+
}
|
|
110
|
+
const ready = state === "current";
|
|
111
|
+
const rebindGuidance = state === "current"
|
|
112
|
+
? "Session is current: bound generation matches live for used surfaces."
|
|
113
|
+
: state === "stale_soft"
|
|
114
|
+
? `Advisory drift (${soft.join(", ") || "soft surfaces"}). Continue with caution; ` +
|
|
115
|
+
`rebind when convenient. ${REBIND_GUIDANCE}`
|
|
116
|
+
: `Hard drift (generation and/or ${hard.join(", ") || "payload surfaces"}). ` +
|
|
117
|
+
`Rebind before trusted work. ${REBIND_GUIDANCE}`;
|
|
118
|
+
return {
|
|
119
|
+
boundGeneration: bound.boundGeneration,
|
|
120
|
+
liveGeneration: live.generation,
|
|
121
|
+
boundContentVersion: bound.contentVersion,
|
|
122
|
+
liveContentVersion: live.contentVersion,
|
|
123
|
+
state,
|
|
124
|
+
differingSurfaces: diffs,
|
|
125
|
+
hardDiffs: hard,
|
|
126
|
+
softDiffs: soft,
|
|
127
|
+
ready,
|
|
128
|
+
rebindGuidance,
|
|
129
|
+
midMissionSafety: MID_MISSION_SAFETY,
|
|
130
|
+
live,
|
|
131
|
+
bound,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live deposit generation token (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Stamped on successful deposit apply/refresh as a monotonic integer so long-lived
|
|
5
|
+
* sessions can compare bound vs live without host-specific session APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { type FreshnessSurface, type LiveGeneration, type SurfaceFingerprints } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Relative path under project for the live generation token.
|
|
10
|
+
*
|
|
11
|
+
* Lives under `.deft/` (not `.deft/core/`) so a full-tree payload replace
|
|
12
|
+
* (`replaceTree` on update) does not wipe the monotonic counter (#3117).
|
|
13
|
+
*/
|
|
14
|
+
export declare const LIVE_GENERATION_REL: string;
|
|
15
|
+
export declare function liveGenerationPath(projectRoot: string): string;
|
|
16
|
+
export interface StampLiveGenerationOptions {
|
|
17
|
+
readonly contentVersion: string;
|
|
18
|
+
readonly stampedBy: string;
|
|
19
|
+
/**
|
|
20
|
+
* When true, always bump the monotonic counter (payload swapped / init apply).
|
|
21
|
+
* When false, ensure the token exists and matches contentVersion without bumping
|
|
22
|
+
* if already current (already-current refresh path).
|
|
23
|
+
*/
|
|
24
|
+
readonly increment: boolean;
|
|
25
|
+
readonly nowIso?: string;
|
|
26
|
+
readonly surfaces?: SurfaceFingerprints;
|
|
27
|
+
}
|
|
28
|
+
/** Build default surface fingerprints from a content version string. */
|
|
29
|
+
export declare function defaultSurfaceFingerprints(contentVersion: string): SurfaceFingerprints;
|
|
30
|
+
/** Parse a live generation record from disk JSON (or null if invalid/missing). */
|
|
31
|
+
export declare function parseLiveGeneration(raw: unknown): LiveGeneration | null;
|
|
32
|
+
/** Read the live generation token from the deposit (null when absent/unreadable). */
|
|
33
|
+
export declare function readLiveGeneration(projectRoot: string): LiveGeneration | null;
|
|
34
|
+
/**
|
|
35
|
+
* Stamp (or ensure) the live deposit generation token after a successful apply.
|
|
36
|
+
*
|
|
37
|
+
* Monotonic: successful payload swap / init always increments. Already-current
|
|
38
|
+
* refresh ensures a readable token exists without advancing the counter when the
|
|
39
|
+
* content version is unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stampLiveGeneration(projectRoot: string, options: StampLiveGenerationOptions): LiveGeneration;
|
|
42
|
+
/** Surfaces that differ between two fingerprint maps. */
|
|
43
|
+
export declare function differingSurfaces(bound: SurfaceFingerprints | null | undefined, live: SurfaceFingerprints | null | undefined, surfaceSet?: readonly FreshnessSurface[]): FreshnessSurface[];
|
|
44
|
+
//# sourceMappingURL=generation.d.ts.map
|