@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,172 @@
|
|
|
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 { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
|
|
10
|
+
import { FRESHNESS_SCHEMA_VERSION, } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Relative path under project for the live generation token.
|
|
13
|
+
*
|
|
14
|
+
* Lives under `.deft/` (not `.deft/core/`) so a full-tree payload replace
|
|
15
|
+
* (`replaceTree` on update) does not wipe the monotonic counter (#3117).
|
|
16
|
+
*/
|
|
17
|
+
export const LIVE_GENERATION_REL = join(".deft", "GENERATION.json");
|
|
18
|
+
export function liveGenerationPath(projectRoot) {
|
|
19
|
+
return join(resolve(projectRoot), LIVE_GENERATION_REL);
|
|
20
|
+
}
|
|
21
|
+
function nowIsoDefault() {
|
|
22
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
23
|
+
}
|
|
24
|
+
function normalizeVersion(version) {
|
|
25
|
+
return version.trim().replace(/^v/, "");
|
|
26
|
+
}
|
|
27
|
+
/** Build default surface fingerprints from a content version string. */
|
|
28
|
+
export function defaultSurfaceFingerprints(contentVersion) {
|
|
29
|
+
const v = normalizeVersion(contentVersion);
|
|
30
|
+
const tagged = v.startsWith("v") ? v : `v${v}`;
|
|
31
|
+
return {
|
|
32
|
+
payload: v,
|
|
33
|
+
version: tagged,
|
|
34
|
+
templates: v,
|
|
35
|
+
skills: v,
|
|
36
|
+
docs: v,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function isSurfaceFingerprints(value) {
|
|
40
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
for (const [k, v] of Object.entries(value)) {
|
|
44
|
+
if (typeof k !== "string" || typeof v !== "string") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/** Parse a live generation record from disk JSON (or null if invalid/missing). */
|
|
51
|
+
export function parseLiveGeneration(raw) {
|
|
52
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const rec = raw;
|
|
56
|
+
const generation = rec.generation;
|
|
57
|
+
if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const contentVersion = typeof rec.contentVersion === "string" && rec.contentVersion.trim()
|
|
61
|
+
? rec.contentVersion.trim()
|
|
62
|
+
: null;
|
|
63
|
+
if (contentVersion === null) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const stampedAt = typeof rec.stampedAt === "string" && rec.stampedAt.trim() ? rec.stampedAt.trim() : null;
|
|
67
|
+
if (stampedAt === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const stampedBy = typeof rec.stampedBy === "string" && rec.stampedBy.trim() ? rec.stampedBy.trim() : "unknown";
|
|
71
|
+
const surfaces = isSurfaceFingerprints(rec.surfaces)
|
|
72
|
+
? rec.surfaces
|
|
73
|
+
: defaultSurfaceFingerprints(contentVersion);
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: FRESHNESS_SCHEMA_VERSION,
|
|
76
|
+
generation,
|
|
77
|
+
contentVersion: normalizeVersion(contentVersion),
|
|
78
|
+
stampedAt,
|
|
79
|
+
stampedBy,
|
|
80
|
+
surfaces,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Read the live generation token from the deposit (null when absent/unreadable). */
|
|
84
|
+
export function readLiveGeneration(projectRoot) {
|
|
85
|
+
const path = liveGenerationPath(projectRoot);
|
|
86
|
+
if (!existsSync(path)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const text = readFileSync(path, "utf8");
|
|
91
|
+
const parsed = JSON.parse(text);
|
|
92
|
+
return parseLiveGeneration(parsed);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function writeLiveGeneration(projectRoot, token) {
|
|
99
|
+
const root = resolve(projectRoot);
|
|
100
|
+
const target = liveGenerationPath(projectRoot);
|
|
101
|
+
const body = `${JSON.stringify(token, null, 2)}\n`;
|
|
102
|
+
try {
|
|
103
|
+
containedWrite({
|
|
104
|
+
root,
|
|
105
|
+
target,
|
|
106
|
+
data: body,
|
|
107
|
+
mode: "replace",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof ContainedWriteError) {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
return target;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Stamp (or ensure) the live deposit generation token after a successful apply.
|
|
120
|
+
*
|
|
121
|
+
* Monotonic: successful payload swap / init always increments. Already-current
|
|
122
|
+
* refresh ensures a readable token exists without advancing the counter when the
|
|
123
|
+
* content version is unchanged.
|
|
124
|
+
*/
|
|
125
|
+
export function stampLiveGeneration(projectRoot, options) {
|
|
126
|
+
const contentVersion = normalizeVersion(options.contentVersion);
|
|
127
|
+
const surfaces = options.surfaces ?? defaultSurfaceFingerprints(contentVersion);
|
|
128
|
+
const stampedAt = options.nowIso ?? nowIsoDefault();
|
|
129
|
+
const prior = readLiveGeneration(projectRoot);
|
|
130
|
+
let generation;
|
|
131
|
+
if (prior === null) {
|
|
132
|
+
generation = 1;
|
|
133
|
+
}
|
|
134
|
+
else if (options.increment) {
|
|
135
|
+
generation = prior.generation + 1;
|
|
136
|
+
}
|
|
137
|
+
else if (normalizeVersion(prior.contentVersion) !== contentVersion) {
|
|
138
|
+
// Content drifted without an explicit increment flag — still advance.
|
|
139
|
+
generation = prior.generation + 1;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// Already current: do not rewrite GENERATION.json (avoids dirty trees under
|
|
143
|
+
// core.autocrlf=true after idempotent `directive update` — Windows #2118).
|
|
144
|
+
return prior;
|
|
145
|
+
}
|
|
146
|
+
const token = {
|
|
147
|
+
schemaVersion: FRESHNESS_SCHEMA_VERSION,
|
|
148
|
+
generation,
|
|
149
|
+
contentVersion,
|
|
150
|
+
stampedAt,
|
|
151
|
+
stampedBy: options.stampedBy,
|
|
152
|
+
surfaces,
|
|
153
|
+
};
|
|
154
|
+
writeLiveGeneration(projectRoot, token);
|
|
155
|
+
return token;
|
|
156
|
+
}
|
|
157
|
+
/** Surfaces that differ between two fingerprint maps. */
|
|
158
|
+
export function differingSurfaces(bound, live, surfaceSet = ["payload", "version", "templates", "skills", "docs"]) {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const surface of surfaceSet) {
|
|
161
|
+
const b = bound?.[surface] ?? null;
|
|
162
|
+
const l = live?.[surface] ?? null;
|
|
163
|
+
if (b === null && l === null) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (b !== l) {
|
|
167
|
+
out.push(surface);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=generation.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic freshness contract (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Monotonic generation on deposit apply, session bind of that generation, and
|
|
5
|
+
* bound-vs-live reports with current | stale_soft | stale_hard states.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./bind.js";
|
|
8
|
+
export * from "./cli.js";
|
|
9
|
+
export * from "./compare.js";
|
|
10
|
+
export * from "./generation.js";
|
|
11
|
+
export * from "./report.js";
|
|
12
|
+
export * from "./types.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic freshness contract (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Monotonic generation on deposit apply, session bind of that generation, and
|
|
5
|
+
* bound-vs-live reports with current | stale_soft | stale_hard states.
|
|
6
|
+
*/
|
|
7
|
+
export * from "./bind.js";
|
|
8
|
+
export * from "./cli.js";
|
|
9
|
+
export * from "./compare.js";
|
|
10
|
+
export * from "./generation.js";
|
|
11
|
+
export * from "./report.js";
|
|
12
|
+
export * from "./types.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness report assembly + human formatting (#3117).
|
|
3
|
+
*/
|
|
4
|
+
import type { FreshnessReport } from "./types.js";
|
|
5
|
+
export interface ReportFreshnessOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Host session identity. When set, only that session's bind is read
|
|
8
|
+
* (multi-agent isolation).
|
|
9
|
+
*
|
|
10
|
+
* When omitted, recover the current ritual `session_id` from
|
|
11
|
+
* `.deft/ritual-state.json` (written by `session:start`) so bare
|
|
12
|
+
* `freshness:report` matches the bind just created. Pass `null` to force
|
|
13
|
+
* the default project bind path only.
|
|
14
|
+
*/
|
|
15
|
+
readonly sessionId?: string | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve which bind identity a report/bind should use.
|
|
19
|
+
*
|
|
20
|
+
* Precedence:
|
|
21
|
+
* 1. Explicit non-empty `sessionId`
|
|
22
|
+
* 2. `DEFT_SESSION_ID` env (per-process; multi-agent safe)
|
|
23
|
+
* 3. Ritual `session_id` from `.deft/ritual-state.json` (same-worktree operator)
|
|
24
|
+
* 4. `null` → default bind path only
|
|
25
|
+
*
|
|
26
|
+
* Trusted `current`/`ready` still requires a **pinned** identity (explicit or env).
|
|
27
|
+
* Ritual recovery alone is convenience for bind write target, not for trusted ready.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveReportSessionId(projectRoot: string, explicit?: string | null, env?: NodeJS.ProcessEnv): string | null;
|
|
30
|
+
/** True when the caller pinned identity via explicit arg or DEFT_SESSION_ID. */
|
|
31
|
+
export declare function hasPinnedSessionIdentity(explicit?: string | null, env?: NodeJS.ProcessEnv): boolean;
|
|
32
|
+
/** Build a freshness report for the project (bound vs live on disk). */
|
|
33
|
+
export declare function reportFreshness(projectRoot: string, options?: ReportFreshnessOptions): FreshnessReport;
|
|
34
|
+
/** Human-readable multi-line freshness report. */
|
|
35
|
+
export declare function formatFreshnessReport(report: FreshnessReport): string;
|
|
36
|
+
/** JSON-serializable shape for --json output. */
|
|
37
|
+
export declare function freshnessReportToJson(report: FreshnessReport): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Exit code for freshness:report.
|
|
40
|
+
* 0 = current (ready); 1 = stale_soft or unbound (caution); 2 = stale_hard (must rebind).
|
|
41
|
+
*/
|
|
42
|
+
export declare function freshnessReportExitCode(report: FreshnessReport): 0 | 1 | 2;
|
|
43
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freshness report assembly + human formatting (#3117).
|
|
3
|
+
*/
|
|
4
|
+
import { readRitualState } from "../session/ritual-sentinel.js";
|
|
5
|
+
import { readBoundGeneration } from "./bind.js";
|
|
6
|
+
import { compareFreshness } from "./compare.js";
|
|
7
|
+
import { readLiveGeneration } from "./generation.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve which bind identity a report/bind should use.
|
|
10
|
+
*
|
|
11
|
+
* Precedence:
|
|
12
|
+
* 1. Explicit non-empty `sessionId`
|
|
13
|
+
* 2. `DEFT_SESSION_ID` env (per-process; multi-agent safe)
|
|
14
|
+
* 3. Ritual `session_id` from `.deft/ritual-state.json` (same-worktree operator)
|
|
15
|
+
* 4. `null` → default bind path only
|
|
16
|
+
*
|
|
17
|
+
* Trusted `current`/`ready` still requires a **pinned** identity (explicit or env).
|
|
18
|
+
* Ritual recovery alone is convenience for bind write target, not for trusted ready.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveReportSessionId(projectRoot, explicit, env = process.env) {
|
|
21
|
+
if (explicit === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
25
|
+
return explicit.trim();
|
|
26
|
+
}
|
|
27
|
+
const fromEnv = (env.DEFT_SESSION_ID ?? "").trim();
|
|
28
|
+
if (fromEnv.length > 0) {
|
|
29
|
+
return fromEnv;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const [state] = readRitualState(projectRoot);
|
|
33
|
+
const id = state?.sessionId?.trim();
|
|
34
|
+
return id && id.length > 0 ? id : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** True when the caller pinned identity via explicit arg or DEFT_SESSION_ID. */
|
|
41
|
+
export function hasPinnedSessionIdentity(explicit, env = process.env) {
|
|
42
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return (env.DEFT_SESSION_ID ?? "").trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
/** Build a freshness report for the project (bound vs live on disk). */
|
|
48
|
+
export function reportFreshness(projectRoot, options = {}) {
|
|
49
|
+
const sessionId = resolveReportSessionId(projectRoot, options.sessionId);
|
|
50
|
+
const bound = readBoundGeneration(projectRoot, { sessionId });
|
|
51
|
+
const live = readLiveGeneration(projectRoot);
|
|
52
|
+
const report = compareFreshness(bound, live);
|
|
53
|
+
// Multi-agent safety: never claim ready/current without a pinned session identity.
|
|
54
|
+
// Ritual recovery alone can point at another concurrent session's bind on a
|
|
55
|
+
// shared worktree (Greptile). Hosts set DEFT_SESSION_ID or pass --session-id.
|
|
56
|
+
if (report.ready && !hasPinnedSessionIdentity(options.sessionId)) {
|
|
57
|
+
return {
|
|
58
|
+
...report,
|
|
59
|
+
state: "stale_soft",
|
|
60
|
+
ready: false,
|
|
61
|
+
rebindGuidance: "Session identity is not pinned. Set DEFT_SESSION_ID to this session's id " +
|
|
62
|
+
"(printed by session:start) or pass --session-id before trusted work. " +
|
|
63
|
+
"Bare reports without a pinned id cannot certify readiness when multiple " +
|
|
64
|
+
"sessions share a worktree.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Host must attest payload surfaces were loaded for this generation (session:start
|
|
68
|
+
// does; CLI bind requires --confirm-payload-loaded). Disk bind alone is insufficient.
|
|
69
|
+
if (report.ready && bound?.payloadLoaded !== true) {
|
|
70
|
+
return {
|
|
71
|
+
...report,
|
|
72
|
+
state: "stale_soft",
|
|
73
|
+
ready: false,
|
|
74
|
+
rebindGuidance: "Bind has no payload-loaded attestation. Re-load skills/rituals/templates into " +
|
|
75
|
+
"the session, then rebind with payloadLoaded (session:start, or " +
|
|
76
|
+
"`deft freshness:bind -- --confirm-payload-loaded`).",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return report;
|
|
80
|
+
}
|
|
81
|
+
/** Human-readable multi-line freshness report. */
|
|
82
|
+
export function formatFreshnessReport(report) {
|
|
83
|
+
const lines = [
|
|
84
|
+
"[deft freshness] bound vs live generation",
|
|
85
|
+
` state : ${report.state}`,
|
|
86
|
+
` ready : ${report.ready ? "yes" : "no"}`,
|
|
87
|
+
` bound generation : ${report.boundGeneration ?? "(none)"}`,
|
|
88
|
+
` live generation : ${report.liveGeneration ?? "(none)"}`,
|
|
89
|
+
` bound content : ${report.boundContentVersion ?? "(none)"}`,
|
|
90
|
+
` live content : ${report.liveContentVersion ?? "(none)"}`,
|
|
91
|
+
];
|
|
92
|
+
if (report.differingSurfaces.length > 0) {
|
|
93
|
+
lines.push(` differing surfaces: ${report.differingSurfaces.join(", ")}`);
|
|
94
|
+
if (report.hardDiffs.length > 0) {
|
|
95
|
+
lines.push(` hard diffs : ${report.hardDiffs.join(", ")}`);
|
|
96
|
+
}
|
|
97
|
+
if (report.softDiffs.length > 0) {
|
|
98
|
+
lines.push(` soft diffs : ${report.softDiffs.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (report.state !== "unbound") {
|
|
102
|
+
lines.push(" differing surfaces: (none)");
|
|
103
|
+
}
|
|
104
|
+
lines.push(` rebind : ${report.rebindGuidance}`);
|
|
105
|
+
if (report.state === "stale_hard" || report.state === "unbound") {
|
|
106
|
+
lines.push(` mid-mission : ${report.midMissionSafety}`);
|
|
107
|
+
}
|
|
108
|
+
return `${lines.join("\n")}\n`;
|
|
109
|
+
}
|
|
110
|
+
/** JSON-serializable shape for --json output. */
|
|
111
|
+
export function freshnessReportToJson(report) {
|
|
112
|
+
return {
|
|
113
|
+
state: report.state,
|
|
114
|
+
ready: report.ready,
|
|
115
|
+
bound_generation: report.boundGeneration,
|
|
116
|
+
live_generation: report.liveGeneration,
|
|
117
|
+
bound_content_version: report.boundContentVersion,
|
|
118
|
+
live_content_version: report.liveContentVersion,
|
|
119
|
+
differing_surfaces: report.differingSurfaces,
|
|
120
|
+
hard_diffs: report.hardDiffs,
|
|
121
|
+
soft_diffs: report.softDiffs,
|
|
122
|
+
rebind_guidance: report.rebindGuidance,
|
|
123
|
+
mid_mission_safety: report.midMissionSafety,
|
|
124
|
+
live: report.live,
|
|
125
|
+
bound: report.bound,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Exit code for freshness:report.
|
|
130
|
+
* 0 = current (ready); 1 = stale_soft or unbound (caution); 2 = stale_hard (must rebind).
|
|
131
|
+
*/
|
|
132
|
+
export function freshnessReportExitCode(report) {
|
|
133
|
+
if (report.state === "current")
|
|
134
|
+
return 0;
|
|
135
|
+
if (report.state === "stale_hard")
|
|
136
|
+
return 2;
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic freshness contract types (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Bound generation is what a long-lived session loaded; live generation is what
|
|
5
|
+
* the on-disk deposit currently stamps after a successful apply/refresh.
|
|
6
|
+
* Disk-only "up to date" is insufficient for session readiness.
|
|
7
|
+
*/
|
|
8
|
+
/** Schema version for GENERATION.json and session-bind.json. */
|
|
9
|
+
export declare const FRESHNESS_SCHEMA_VERSION: 1;
|
|
10
|
+
/**
|
|
11
|
+
* Freshness state for a session relative to the live deposit.
|
|
12
|
+
*
|
|
13
|
+
* - `current` — bound matches live for used surfaces
|
|
14
|
+
* - `stale_soft` — additive / advisory drift; safe to continue with caution
|
|
15
|
+
* - `stale_hard` — evidence-untrustworthy drift; rebind before trusted work
|
|
16
|
+
* - `unbound` — no session bind recorded (not ready for trusted work)
|
|
17
|
+
*/
|
|
18
|
+
export declare const FRESHNESS_STATES: readonly ["current", "stale_soft", "stale_hard", "unbound"];
|
|
19
|
+
export type FreshnessState = (typeof FRESHNESS_STATES)[number];
|
|
20
|
+
/** Surface ids compared in a freshness report. */
|
|
21
|
+
export declare const FRESHNESS_SURFACES: readonly ["payload", "version", "templates", "skills", "docs"];
|
|
22
|
+
export type FreshnessSurface = (typeof FRESHNESS_SURFACES)[number];
|
|
23
|
+
/** Hard surfaces: drift means evidence is untrustworthy without rebind. */
|
|
24
|
+
export declare const HARD_SURFACES: readonly FreshnessSurface[];
|
|
25
|
+
/** Soft / advisory surfaces: drift is cautionary, not evidence-invalidating alone. */
|
|
26
|
+
export declare const SOFT_SURFACES: readonly FreshnessSurface[];
|
|
27
|
+
/** Fingerprints for each surface at a generation (typically content version tags). */
|
|
28
|
+
export type SurfaceFingerprints = Readonly<Partial<Record<FreshnessSurface, string>>>;
|
|
29
|
+
/** Live deposit generation token written on successful apply/refresh. */
|
|
30
|
+
export interface LiveGeneration {
|
|
31
|
+
readonly schemaVersion: typeof FRESHNESS_SCHEMA_VERSION;
|
|
32
|
+
readonly generation: number;
|
|
33
|
+
readonly contentVersion: string;
|
|
34
|
+
readonly stampedAt: string;
|
|
35
|
+
readonly stampedBy: string;
|
|
36
|
+
readonly surfaces: SurfaceFingerprints;
|
|
37
|
+
}
|
|
38
|
+
/** Session bind of the generation loaded into runtime context. */
|
|
39
|
+
export interface BoundGeneration {
|
|
40
|
+
readonly schemaVersion: typeof FRESHNESS_SCHEMA_VERSION;
|
|
41
|
+
readonly boundGeneration: number;
|
|
42
|
+
readonly boundAt: string;
|
|
43
|
+
readonly contentVersion: string;
|
|
44
|
+
readonly surfaces: SurfaceFingerprints;
|
|
45
|
+
/** Optional host-supplied session identity (host-agnostic; not a host key scheme). */
|
|
46
|
+
readonly sessionId?: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Host attestation that payload surfaces for this generation were loaded into
|
|
49
|
+
* the session before bind. Required for trusted `current`/`ready`.
|
|
50
|
+
* session:start sets true; bare CLI bind requires `--confirm-payload-loaded`.
|
|
51
|
+
*/
|
|
52
|
+
readonly payloadLoaded?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Full freshness report comparing bound vs live. */
|
|
55
|
+
export interface FreshnessReport {
|
|
56
|
+
readonly boundGeneration: number | null;
|
|
57
|
+
readonly liveGeneration: number | null;
|
|
58
|
+
readonly boundContentVersion: string | null;
|
|
59
|
+
readonly liveContentVersion: string | null;
|
|
60
|
+
readonly state: FreshnessState;
|
|
61
|
+
readonly differingSurfaces: readonly FreshnessSurface[];
|
|
62
|
+
readonly hardDiffs: readonly FreshnessSurface[];
|
|
63
|
+
readonly softDiffs: readonly FreshnessSurface[];
|
|
64
|
+
readonly ready: boolean;
|
|
65
|
+
readonly rebindGuidance: string;
|
|
66
|
+
readonly midMissionSafety: string;
|
|
67
|
+
readonly live: LiveGeneration | null;
|
|
68
|
+
readonly bound: BoundGeneration | null;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-agnostic freshness contract types (#3117).
|
|
3
|
+
*
|
|
4
|
+
* Bound generation is what a long-lived session loaded; live generation is what
|
|
5
|
+
* the on-disk deposit currently stamps after a successful apply/refresh.
|
|
6
|
+
* Disk-only "up to date" is insufficient for session readiness.
|
|
7
|
+
*/
|
|
8
|
+
/** Schema version for GENERATION.json and session-bind.json. */
|
|
9
|
+
export const FRESHNESS_SCHEMA_VERSION = 1;
|
|
10
|
+
/**
|
|
11
|
+
* Freshness state for a session relative to the live deposit.
|
|
12
|
+
*
|
|
13
|
+
* - `current` — bound matches live for used surfaces
|
|
14
|
+
* - `stale_soft` — additive / advisory drift; safe to continue with caution
|
|
15
|
+
* - `stale_hard` — evidence-untrustworthy drift; rebind before trusted work
|
|
16
|
+
* - `unbound` — no session bind recorded (not ready for trusted work)
|
|
17
|
+
*/
|
|
18
|
+
export const FRESHNESS_STATES = ["current", "stale_soft", "stale_hard", "unbound"];
|
|
19
|
+
/** Surface ids compared in a freshness report. */
|
|
20
|
+
export const FRESHNESS_SURFACES = ["payload", "version", "templates", "skills", "docs"];
|
|
21
|
+
/** Hard surfaces: drift means evidence is untrustworthy without rebind. */
|
|
22
|
+
export const HARD_SURFACES = [
|
|
23
|
+
"payload",
|
|
24
|
+
"version",
|
|
25
|
+
"templates",
|
|
26
|
+
"skills",
|
|
27
|
+
];
|
|
28
|
+
/** Soft / advisory surfaces: drift is cautionary, not evidence-invalidating alone. */
|
|
29
|
+
export const SOFT_SURFACES = ["docs"];
|
|
30
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff evidence bound-proof contract (#3120).
|
|
3
|
+
*
|
|
4
|
+
* Makes invented remote artifact claims an illegal shape under status pass.
|
|
5
|
+
*/
|
|
6
|
+
export type { AxisEvidence, AxisState, HandoffEvidence, HandoffEvidenceValidation, HandoffFailClass, ProofStatus, RemoteProbe, } from "./validate.js";
|
|
7
|
+
export { canClaimPass, detectRemoteClaims, probeBindsClaim, validateHandoffEvidence, } from "./validate.js";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handoff evidence bound-proof contract (#3120).
|
|
3
|
+
*
|
|
4
|
+
* Makes invented remote artifact claims an illegal shape under status pass.
|
|
5
|
+
*/
|
|
6
|
+
export { canClaimPass, detectRemoteClaims, probeBindsClaim, validateHandoffEvidence, } from "./validate.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic handoff-evidence validator (#3120).
|
|
3
|
+
*
|
|
4
|
+
* status pass is forbidden when any remote artifact is claimed unless
|
|
5
|
+
* proof_status is bound and same-turn probe snippets cover each claim.
|
|
6
|
+
* Invented-done (false remote artifacts under pass) ranks stricter than
|
|
7
|
+
* empty-done (pass with no completion substance).
|
|
8
|
+
*/
|
|
9
|
+
/** Binding state for remote artifact claims in handoff evidence. */
|
|
10
|
+
export type ProofStatus = "bound" | "unbound" | "n/a-no-remote-claim";
|
|
11
|
+
/** Work / ship / gate axis state (local → forge → gates). */
|
|
12
|
+
export type AxisState = "done" | "in_progress" | "not_started" | "blocked" | "n/a";
|
|
13
|
+
/** Fail ranking: invented-done is stricter than empty-done. */
|
|
14
|
+
export type HandoffFailClass = "none" | "empty-done" | "unbound-remote-claim" | "invented-done" | "shape-error";
|
|
15
|
+
/** Same-turn live probe that binds a remote claim. */
|
|
16
|
+
export interface RemoteProbe {
|
|
17
|
+
/** Command run this turn (e.g. `gh api repos/.../pulls/N`). */
|
|
18
|
+
readonly command: string;
|
|
19
|
+
/** Short raw snippet from that command's stdout (not narration). */
|
|
20
|
+
readonly snippet: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AxisEvidence {
|
|
23
|
+
readonly state?: AxisState | string;
|
|
24
|
+
readonly notes?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Portable handoff evidence block for builder / pre-pr / review-cycle exits.
|
|
28
|
+
* Free-text remote fields without bound probes are an illegal shape under pass.
|
|
29
|
+
*/
|
|
30
|
+
export interface HandoffEvidence {
|
|
31
|
+
/** Overall process status. `pass` is the strictest gate for remote claims. */
|
|
32
|
+
readonly status: "pass" | "fail" | "blocked" | "partial" | string;
|
|
33
|
+
/**
|
|
34
|
+
* Binding state. Required semantics:
|
|
35
|
+
* - `bound` when any remote claim is present and probes bind them
|
|
36
|
+
* - `n/a-no-remote-claim` when no remote PR/SHA/CI/review claim is made
|
|
37
|
+
* - `unbound` is never valid under status pass with remote claims
|
|
38
|
+
*/
|
|
39
|
+
readonly proof_status?: ProofStatus | string;
|
|
40
|
+
/** Local work state (edits / tests / commits on the branch). */
|
|
41
|
+
readonly work?: AxisEvidence;
|
|
42
|
+
/** Ship state (pushed branch / PR exists). */
|
|
43
|
+
readonly ship?: AxisEvidence;
|
|
44
|
+
/** Gate state (CI / review on claimed HEAD). */
|
|
45
|
+
readonly gate?: AxisEvidence;
|
|
46
|
+
/** Remote PR URL claim (requires pr probe when set). */
|
|
47
|
+
readonly pr_url?: string | null;
|
|
48
|
+
/** Remote PR number claim (requires pr probe when set). */
|
|
49
|
+
readonly pr_number?: number | string | null;
|
|
50
|
+
/** Commit / HEAD SHA claim (requires sha probe when set). */
|
|
51
|
+
readonly commit_sha?: string | null;
|
|
52
|
+
/** Alias for commit_sha. */
|
|
53
|
+
readonly head_sha?: string | null;
|
|
54
|
+
/**
|
|
55
|
+
* CI claim. Values like green/pass/success/ok are remote claims.
|
|
56
|
+
* Neutral/unknown/pending without green semantics are not treated as claims.
|
|
57
|
+
*/
|
|
58
|
+
readonly ci_status?: string | null;
|
|
59
|
+
/** Review score / confidence claim (requires review probe when set). */
|
|
60
|
+
readonly review_score?: number | string | null;
|
|
61
|
+
/** Same-turn probes that bind remote claims. */
|
|
62
|
+
readonly probes?: {
|
|
63
|
+
readonly pr?: RemoteProbe | null;
|
|
64
|
+
readonly sha?: RemoteProbe | null;
|
|
65
|
+
readonly ci?: RemoteProbe | null;
|
|
66
|
+
readonly review?: RemoteProbe | null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface HandoffEvidenceValidation {
|
|
70
|
+
readonly ok: boolean;
|
|
71
|
+
readonly failClass: HandoffFailClass;
|
|
72
|
+
readonly reasons: readonly string[];
|
|
73
|
+
/** True when any remote PR/SHA/CI/review claim was detected. */
|
|
74
|
+
readonly hasRemoteClaims: boolean;
|
|
75
|
+
/** Claim keys that lack a binding probe. */
|
|
76
|
+
readonly unboundClaims: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* True when the probe's raw snippet actually mentions the claimed remote value
|
|
80
|
+
* with token-level binding (not incidental substring collisions).
|
|
81
|
+
* Never throws on malformed claims — returns false instead.
|
|
82
|
+
*
|
|
83
|
+
* Scope: textual shape binding only. Live probe execution/provenance is agent
|
|
84
|
+
* process responsibility (preamble probe-then-fill); this library does not
|
|
85
|
+
* re-run forge/git commands.
|
|
86
|
+
*/
|
|
87
|
+
export declare function probeBindsClaim(claim: string, evidence: HandoffEvidence, probe: RemoteProbe | null | undefined): boolean;
|
|
88
|
+
/** Detect which remote claim keys are present on the evidence object. */
|
|
89
|
+
export declare function detectRemoteClaims(evidence: HandoffEvidence): string[];
|
|
90
|
+
/**
|
|
91
|
+
* Validate handoff evidence against the #3120 bound-proof contract.
|
|
92
|
+
*
|
|
93
|
+
* Ranking when invalid under status pass:
|
|
94
|
+
* 1. invented-done — remote claims present without bound proof
|
|
95
|
+
* 2. empty-done — pass with no completion substance and no remote claims
|
|
96
|
+
* 3. unbound-remote-claim — non-pass status but still has unbound remote claims
|
|
97
|
+
* 4. shape-error — malformed proof_status / inconsistent n/a
|
|
98
|
+
*/
|
|
99
|
+
export declare function validateHandoffEvidence(evidence: HandoffEvidence): HandoffEvidenceValidation;
|
|
100
|
+
/**
|
|
101
|
+
* Convenience: true when status may legally be pass for this evidence.
|
|
102
|
+
* Equivalent to validateHandoffEvidence(...).ok under a pass status intent.
|
|
103
|
+
*/
|
|
104
|
+
export declare function canClaimPass(evidence: HandoffEvidence): boolean;
|
|
105
|
+
//# sourceMappingURL=validate.d.ts.map
|