@deftai/directive-core 0.98.0 → 0.99.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 +400 -56
- package/dist/consumer-check-contract/evaluate.d.ts +40 -0
- package/dist/consumer-check-contract/evaluate.js +188 -3
- package/dist/consumer-check-contract/index.d.ts +1 -1
- package/dist/consumer-check-contract/index.js +1 -1
- package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
- package/dist/content-contracts/skills/greptile-detector.js +202 -4
- package/dist/decision/index.d.ts +17 -0
- package/dist/decision/index.js +35 -0
- package/dist/decision/list.d.ts +47 -0
- package/dist/decision/list.js +250 -0
- package/dist/decision/schema.d.ts +88 -0
- package/dist/decision/schema.js +293 -0
- package/dist/decision/write.d.ts +82 -0
- package/dist/decision/write.js +427 -0
- package/dist/eval/report.d.ts +29 -0
- package/dist/eval/report.js +69 -0
- package/dist/eval/run.d.ts +9 -0
- package/dist/eval/run.js +40 -4
- package/dist/eval/version-pin.d.ts +99 -0
- package/dist/eval/version-pin.js +181 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/platform/host-content-surface.d.ts +74 -0
- package/dist/platform/host-content-surface.js +214 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/policy/ceremony-dial.d.ts +233 -0
- package/dist/policy/ceremony-dial.js +829 -0
- package/dist/policy/deft-directive-disable.js +12 -2
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/pr-merge-readiness/evaluate.js +10 -0
- package/dist/pr-merge-readiness/mergeability.js +5 -0
- package/dist/pr-merge-readiness/output.js +2 -0
- package/dist/pr-merge-readiness/parse.js +4 -0
- package/dist/pr-merge-readiness/types.d.ts +6 -0
- package/dist/scope/effort-activate-gate.d.ts +28 -0
- package/dist/scope/effort-activate-gate.js +64 -0
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/transition.js +8 -0
- package/dist/scope-provenance/evaluate.d.ts +21 -0
- package/dist/scope-provenance/evaluate.js +143 -33
- package/dist/scope-provenance/index.d.ts +1 -1
- package/dist/scope-provenance/index.js +1 -1
- package/dist/session/session-start.d.ts +24 -1
- package/dist/session/session-start.js +183 -26
- package/dist/swarm/index.d.ts +2 -0
- package/dist/swarm/index.js +2 -0
- package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
- package/dist/swarm/pre-dispatch-cli.js +143 -0
- package/dist/swarm/pre-dispatch.d.ts +87 -0
- package/dist/swarm/pre-dispatch.js +373 -0
- package/dist/vbrief-activate/activate.js +6 -0
- package/dist/vbrief-validate/constants.d.ts +2 -0
- package/dist/vbrief-validate/constants.js +2 -0
- package/dist/vbrief-validate/schema.js +4 -1
- package/package.json +15 -3
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
|
|
20
20
|
*/
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
|
-
import { statSync } from "node:fs";
|
|
22
|
+
import { lstatSync, statSync } from "node:fs";
|
|
23
23
|
import { join, resolve } from "node:path";
|
|
24
24
|
import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
|
|
25
25
|
/** Canonical root-only filename (lowercase). Presence = candidate flag. */
|
|
@@ -51,9 +51,19 @@ const GIT_TRACKED_PROBE_TIMEOUT_MS = 1500;
|
|
|
51
51
|
/** Process-local cache: avoid re-spawning git on every PreToolUse while the flag is present. */
|
|
52
52
|
const trackedProbeCache = new Map();
|
|
53
53
|
const TRACKED_PROBE_CACHE_TTL_MS = 30_000;
|
|
54
|
+
/**
|
|
55
|
+
* Regular-file check that does **not** treat a symlink as an intentional kill-switch
|
|
56
|
+
* (#3213 residual after #3206). `statSync().isFile()` follows links, so
|
|
57
|
+
* `ln -sf /etc/hosts .deft-directive-disable` would activate enforcement off.
|
|
58
|
+
* Prefer `lstatSync`: only a non-symlink regular file counts as present.
|
|
59
|
+
*/
|
|
54
60
|
function defaultIsFile(path) {
|
|
55
61
|
try {
|
|
56
|
-
|
|
62
|
+
const lst = lstatSync(path);
|
|
63
|
+
// Attacker-planted symlink → not an intentional operator flag (#3213).
|
|
64
|
+
if (lst.isSymbolicLink())
|
|
65
|
+
return false;
|
|
66
|
+
return lst.isFile();
|
|
57
67
|
}
|
|
58
68
|
catch {
|
|
59
69
|
return false;
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./agents-md-advisory.js";
|
|
2
2
|
export * from "./autonomy.js";
|
|
3
3
|
export * from "./capacity.js";
|
|
4
|
+
export * from "./ceremony-dial.js";
|
|
4
5
|
export * from "./check-resume.js";
|
|
5
6
|
export * from "./coverage-check-resume-presets.js";
|
|
6
7
|
export * from "./coverage-debt.js";
|
package/dist/policy/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FIELD_HOST_SKILL_DISCOVERY, FIELD_HOST_SKILL_DISCOVERY_CLI_ALIAS, inspectHostSkillDiscovery, } from "../init-deposit/skill-discovery-hosts.js";
|
|
2
2
|
import { FIELD_OPENCLAW_PRODUCT_COMMANDS, FIELD_OPENCLAW_PRODUCT_COMMANDS_CLI_ALIAS, inspectOpenClawProductCommands, } from "../slash/openclaw-deposit.js";
|
|
3
|
+
import { FIELD_CEREMONY_DIAL, FIELD_CEREMONY_DIAL_CLI_ALIAS, inspectCeremonyDial, } from "./ceremony-dial.js";
|
|
3
4
|
import { FIELD_CHECK_RESUME, FIELD_CHECK_RESUME_CLI_ALIAS, inspectCheckResume, } from "./check-resume.js";
|
|
4
5
|
import { FIELD_COVERAGE_DEBT, FIELD_COVERAGE_DEBT_CLI_ALIAS, inspectCoverageDebt, } from "./coverage-debt.js";
|
|
5
6
|
import { FIELD_DELIVERY_BRANCH, FIELD_DELIVERY_BRANCH_CLI_ALIAS, inspectDeliveryBranch, } from "./delivery-branch.js";
|
|
@@ -18,6 +19,7 @@ import { DEFAULT_WIP_CAP } from "./wip.js";
|
|
|
18
19
|
export * from "./agents-md-advisory.js";
|
|
19
20
|
export * from "./autonomy.js";
|
|
20
21
|
export * from "./capacity.js";
|
|
22
|
+
export * from "./ceremony-dial.js";
|
|
21
23
|
export * from "./check-resume.js";
|
|
22
24
|
export * from "./coverage-check-resume-presets.js";
|
|
23
25
|
export * from "./coverage-debt.js";
|
|
@@ -408,6 +410,15 @@ function inspectMinGreptileConfidenceField(data, projectRoot) {
|
|
|
408
410
|
source: field.source,
|
|
409
411
|
};
|
|
410
412
|
}
|
|
413
|
+
function inspectCeremonyDialField(data, projectRoot) {
|
|
414
|
+
const field = inspectCeremonyDial(data, projectRoot);
|
|
415
|
+
return {
|
|
416
|
+
name: field.name,
|
|
417
|
+
current: field.current,
|
|
418
|
+
default: field.default,
|
|
419
|
+
source: field.source,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
411
422
|
const REGISTERED_POLICIES = [
|
|
412
423
|
inspectAllowDirectCommits,
|
|
413
424
|
inspectWipCap,
|
|
@@ -435,6 +446,7 @@ const REGISTERED_POLICIES = [
|
|
|
435
446
|
inspectCheckResumeField,
|
|
436
447
|
inspectRequireHumanMergeField,
|
|
437
448
|
inspectHotfixCriteriaField,
|
|
449
|
+
inspectCeremonyDialField,
|
|
438
450
|
];
|
|
439
451
|
/** Walk registered inspectors and return one row per field (#1148). */
|
|
440
452
|
export function inspectAllPolicies(projectRoot) {
|
|
@@ -471,7 +483,9 @@ export function inspectOnePolicy(name, projectRoot) {
|
|
|
471
483
|
? FIELD_DELIVERY_BRANCH
|
|
472
484
|
: name === FIELD_MIN_GREPTILE_CONFIDENCE_CLI_ALIAS
|
|
473
485
|
? FIELD_MIN_GREPTILE_CONFIDENCE
|
|
474
|
-
: name
|
|
486
|
+
: name === FIELD_CEREMONY_DIAL_CLI_ALIAS
|
|
487
|
+
? FIELD_CEREMONY_DIAL
|
|
488
|
+
: name;
|
|
475
489
|
for (const field of inspectAllPolicies(projectRoot)) {
|
|
476
490
|
if (field.name === normalized)
|
|
477
491
|
return field;
|
|
@@ -39,6 +39,16 @@ export function evaluateGates(_prNumber, headSha, verdict, inline = null, option
|
|
|
39
39
|
"Address remaining findings or push clarifying changes. " +
|
|
40
40
|
"Inspect the floor via `task policy:show --field=minGreptileConfidence` (#3095).");
|
|
41
41
|
}
|
|
42
|
+
// #3225: advisory should-not-merge prose blocks regardless of formal review
|
|
43
|
+
// state / Ready-to-merge mechanical box. Composes with minGreptileConfidence
|
|
44
|
+
// (#3095): sub-threshold conf already fails above; this catches high-conf
|
|
45
|
+
// prose blocks and conf-only paths that still name should-not-merge.
|
|
46
|
+
if (verdict.shouldNotMerge) {
|
|
47
|
+
failures.push("Reviewer bot comment prose records a should-not-merge / not-safe-to-merge " +
|
|
48
|
+
"advisory verdict (no formal Changes-Requested required). " +
|
|
49
|
+
"Mechanical Ready-to-merge / green checks are necessary, never sufficient (#3225). " +
|
|
50
|
+
"Address residual risk or wait for a clean advisory re-review before merge.");
|
|
51
|
+
}
|
|
42
52
|
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
43
53
|
failures.push(`Greptile reports ${verdict.p0Count} P0 and ${verdict.p1Count} P1 findings ` +
|
|
44
54
|
"on the current HEAD. All P0 / P1 findings MUST be addressed before merge " +
|
|
@@ -114,6 +114,11 @@ minConfidence = 4) {
|
|
|
114
114
|
!meetsMinGreptileConfidence(verdict.confidence, minConfidence)) {
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
117
|
+
// Advisory should-not-merge prose is never soft (#3225): GitHub Ready-to-merge
|
|
118
|
+
// must not reconcile away explicit bot prose while formal review stays Comment.
|
|
119
|
+
if (verdict.shouldNotMerge) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
117
122
|
if (verdict.p0Count > 0 || verdict.p1Count > 0) {
|
|
118
123
|
return false;
|
|
119
124
|
}
|
|
@@ -12,6 +12,7 @@ function verdictToDict(verdict) {
|
|
|
12
12
|
p2_count: verdict.p2Count,
|
|
13
13
|
informal_clean: verdict.informalClean,
|
|
14
14
|
excluded_author: verdict.excludedAuthor,
|
|
15
|
+
should_not_merge: verdict.shouldNotMerge,
|
|
15
16
|
raw_body_excerpt: verdict.rawBodyExcerpt,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
@@ -58,6 +59,7 @@ export function printHuman(result) {
|
|
|
58
59
|
lines.push(` Confidence: ${confidenceStr}/5`);
|
|
59
60
|
lines.push(` Findings: P0=${result.verdict.p0Count} ` +
|
|
60
61
|
`P1=${result.verdict.p1Count} P2=${result.verdict.p2Count}`);
|
|
62
|
+
lines.push(` Advisory should-not-merge: ${result.verdict.shouldNotMerge ? "True" : "False"} (#3225)`);
|
|
61
63
|
lines.push(` Errored sentinel: ${result.verdict.errored ? "True" : "False"}`);
|
|
62
64
|
}
|
|
63
65
|
const ciBlock = result.partialData.ci;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasShouldNotMergeProse } from "../content-contracts/skills/greptile-detector.js";
|
|
1
2
|
import { findLastReviewedCommitSha } from "../text/redos-safe.js";
|
|
2
3
|
import { CONFIDENCE_RE, GREPTILE_ERRORED_SENTINEL, GREPTILE_EXCLUDED_AUTHOR_PHRASE, GREPTILE_STATUS_MARKER, INFORMAL_CLEAN_SIGNAL_RE, P0_BADGE, P1_BADGE, SECTION_RE, } from "./constants.js";
|
|
3
4
|
export function emptyVerdict() {
|
|
@@ -11,6 +12,7 @@ export function emptyVerdict() {
|
|
|
11
12
|
p2Count: 0,
|
|
12
13
|
informalClean: false,
|
|
13
14
|
excludedAuthor: false,
|
|
15
|
+
shouldNotMerge: false,
|
|
14
16
|
rawBodyExcerpt: "",
|
|
15
17
|
};
|
|
16
18
|
}
|
|
@@ -81,6 +83,8 @@ export function parseGreptileBody(body) {
|
|
|
81
83
|
p2Count,
|
|
82
84
|
informalClean: false,
|
|
83
85
|
excludedAuthor,
|
|
86
|
+
// #3225: advisory prose is a first-class hard block (not formal review state).
|
|
87
|
+
shouldNotMerge: hasShouldNotMergeProse(body),
|
|
84
88
|
rawBodyExcerpt: body.slice(0, 200),
|
|
85
89
|
};
|
|
86
90
|
if (isInformalCleanMissingCanonicalFields(verdict, body)) {
|
|
@@ -9,6 +9,12 @@ export interface GreptileVerdict {
|
|
|
9
9
|
readonly informalClean: boolean;
|
|
10
10
|
/** Greptile deliberately skipped review for an excluded PR author (#2375). */
|
|
11
11
|
readonly excludedAuthor: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Advisory should-not-merge / not-safe-to-merge prose in the bot comment body
|
|
14
|
+
* (#3225). Independent of formal Changes-Requested review state; a hard block
|
|
15
|
+
* for merge-ready even when GitHub reports Ready-to-merge.
|
|
16
|
+
*/
|
|
17
|
+
readonly shouldNotMerge: boolean;
|
|
12
18
|
readonly rawBodyExcerpt: string;
|
|
13
19
|
}
|
|
14
20
|
export interface GateResult {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort estimate activate gate (#1581).
|
|
3
|
+
*
|
|
4
|
+
* PlanItem.effort is optional (S|M|L|XL). XL means "needs breakdown" —
|
|
5
|
+
* a scope must not move into active/running while any plan item (nested
|
|
6
|
+
* via items or subItems) still carries effort === "XL".
|
|
7
|
+
*/
|
|
8
|
+
export declare const EFFORT_XL: "XL";
|
|
9
|
+
export interface XlEffortHit {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly title: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
export interface EffortActivateGateResult {
|
|
15
|
+
readonly ok: boolean;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
readonly xlItems: readonly XlEffortHit[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Walk plan.items / subItems and collect every item with effort === "XL".
|
|
21
|
+
*/
|
|
22
|
+
export declare function collectXlEffortItems(items: unknown, pathPrefix?: string): XlEffortHit[];
|
|
23
|
+
/**
|
|
24
|
+
* Fail-closed activate gate: any XL plan item blocks pending → active.
|
|
25
|
+
* Omitted effort is allowed (field is optional).
|
|
26
|
+
*/
|
|
27
|
+
export declare function evaluateEffortActivateGate(plan: Record<string, unknown>): EffortActivateGateResult;
|
|
28
|
+
//# sourceMappingURL=effort-activate-gate.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effort estimate activate gate (#1581).
|
|
3
|
+
*
|
|
4
|
+
* PlanItem.effort is optional (S|M|L|XL). XL means "needs breakdown" —
|
|
5
|
+
* a scope must not move into active/running while any plan item (nested
|
|
6
|
+
* via items or subItems) still carries effort === "XL".
|
|
7
|
+
*/
|
|
8
|
+
export const EFFORT_XL = "XL";
|
|
9
|
+
function itemLabel(item) {
|
|
10
|
+
if (typeof item.id === "string" && item.id.length > 0) {
|
|
11
|
+
return item.id;
|
|
12
|
+
}
|
|
13
|
+
if (typeof item.title === "string" && item.title.length > 0) {
|
|
14
|
+
return item.title;
|
|
15
|
+
}
|
|
16
|
+
return "<no-id>";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Walk plan.items / subItems and collect every item with effort === "XL".
|
|
20
|
+
*/
|
|
21
|
+
export function collectXlEffortItems(items, pathPrefix = "plan.items") {
|
|
22
|
+
if (!Array.isArray(items)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const hits = [];
|
|
26
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
27
|
+
const raw = items[i];
|
|
28
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const item = raw;
|
|
32
|
+
const id = itemLabel(item);
|
|
33
|
+
const path = `${pathPrefix}[${id}]`;
|
|
34
|
+
if (item.effort === EFFORT_XL) {
|
|
35
|
+
hits.push({
|
|
36
|
+
id,
|
|
37
|
+
title: typeof item.title === "string" ? item.title : id,
|
|
38
|
+
path,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
hits.push(...collectXlEffortItems(item.items, `${path}.items`));
|
|
42
|
+
hits.push(...collectXlEffortItems(item.subItems, `${path}.subItems`));
|
|
43
|
+
}
|
|
44
|
+
return hits;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fail-closed activate gate: any XL plan item blocks pending → active.
|
|
48
|
+
* Omitted effort is allowed (field is optional).
|
|
49
|
+
*/
|
|
50
|
+
export function evaluateEffortActivateGate(plan) {
|
|
51
|
+
const xlItems = collectXlEffortItems(plan.items);
|
|
52
|
+
if (xlItems.length === 0) {
|
|
53
|
+
return { ok: true, message: "", xlItems: [] };
|
|
54
|
+
}
|
|
55
|
+
const listing = xlItems.map((h) => `${h.path} ("${h.title}")`).join("; ");
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: `Refusing activate: plan item(s) still have effort=XL and must be broken ` +
|
|
59
|
+
`into S/M/L before active/running (#1581): ${listing}. ` +
|
|
60
|
+
`Replace each XL item with smaller S/M/L sub-items (or re-estimate to S/M/L), then retry.`,
|
|
61
|
+
xlItems,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=effort-activate-gate.js.map
|
package/dist/scope/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./constants.js";
|
|
|
4
4
|
export * from "./decomposed-refs.js";
|
|
5
5
|
export * from "./delivery-evidence.js";
|
|
6
6
|
export * from "./demote.js";
|
|
7
|
+
export * from "./effort-activate-gate.js";
|
|
7
8
|
export * from "./main.js";
|
|
8
9
|
export * from "./open-umbrella-warning.js";
|
|
9
10
|
export * from "./project-context.js";
|
package/dist/scope/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./constants.js";
|
|
|
4
4
|
export * from "./decomposed-refs.js";
|
|
5
5
|
export * from "./delivery-evidence.js";
|
|
6
6
|
export * from "./demote.js";
|
|
7
|
+
export * from "./effort-activate-gate.js";
|
|
7
8
|
export * from "./main.js";
|
|
8
9
|
export * from "./open-umbrella-warning.js";
|
|
9
10
|
export * from "./project-context.js";
|
package/dist/scope/transition.js
CHANGED
|
@@ -9,6 +9,7 @@ import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
|
9
9
|
import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
|
|
10
10
|
import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
|
|
11
11
|
import { classifyStoredDeliveryDisposition, evaluateDeliveryGate, stampDeliveryProvenance, } from "./delivery-evidence.js";
|
|
12
|
+
import { evaluateEffortActivateGate } from "./effort-activate-gate.js";
|
|
12
13
|
import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
|
|
13
14
|
import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
|
|
14
15
|
import { utcNowIso } from "./vbrief-json.js";
|
|
@@ -138,6 +139,13 @@ export function runTransition(action, filePath, now = new Date(), options = {})
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
const nowIso = utcNowIso(now);
|
|
142
|
+
// #1581: fail closed before activating a scope that still has XL plan items.
|
|
143
|
+
if (act === "activate") {
|
|
144
|
+
const effortGate = evaluateEffortActivateGate(planObj);
|
|
145
|
+
if (!effortGate.ok) {
|
|
146
|
+
return { ok: false, message: effortGate.message };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
141
149
|
// #3041: fail closed before mutating a code-bearing complete without delivery evidence.
|
|
142
150
|
if (act === "complete") {
|
|
143
151
|
const gate = evaluateDeliveryGate({
|
|
@@ -61,6 +61,27 @@ export declare function normalizeRepoRelPath(p: string): string;
|
|
|
61
61
|
* Git C-quoting / slash folding may produce (Greptile conf=4 residual).
|
|
62
62
|
*/
|
|
63
63
|
export declare function changedSetHasPath(changedSet: ReadonlySet<string>, rel: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
|
|
66
|
+
* Returns null when schema fields required for authorization are missing/malformed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseApprovedScopeRecordRaw(raw: string): ApprovedScopeRecord | null;
|
|
69
|
+
/**
|
|
70
|
+
* True when the merge-base approved-scope record authorizes the current scope and
|
|
71
|
+
* the current disk record is semantically unchanged from that base authority (#3205).
|
|
72
|
+
*
|
|
73
|
+
* Authority comes from the approval record on the base, not from whether the active
|
|
74
|
+
* xBRIEF path existed on the base (pending→active is the normal first activation).
|
|
75
|
+
*/
|
|
76
|
+
export declare function baseApprovalAuthorizesCurrent(input: {
|
|
77
|
+
readonly projectRoot: string;
|
|
78
|
+
readonly baseRef: string | null;
|
|
79
|
+
readonly approvalRecordRel: string;
|
|
80
|
+
readonly planId: string;
|
|
81
|
+
readonly xbriefRelPath: string;
|
|
82
|
+
readonly currentDigest: string;
|
|
83
|
+
readonly currentApproved: ApprovedScopeRecord;
|
|
84
|
+
}): boolean;
|
|
64
85
|
/**
|
|
65
86
|
* Pure evaluation of one active xBRIEF against its approved baseline.
|
|
66
87
|
* Exported for unit tests without git.
|
|
@@ -203,9 +203,83 @@ function listActiveXbriefPaths(projectRoot) {
|
|
|
203
203
|
}
|
|
204
204
|
function remediationForExpansion() {
|
|
205
205
|
return ("Renew human approval: re-record the approved-scope digest after operator review " +
|
|
206
|
-
"(`task scope:record-approved-scope
|
|
207
|
-
"with humanApproval stamp).
|
|
208
|
-
"
|
|
206
|
+
"(`task scope:record-approved-scope -- <xbrief-path> --actor <you>` writes " +
|
|
207
|
+
"`.deft/approved-scope/<plan-id>.json` with a humanApproval stamp). Commit that " +
|
|
208
|
+
"approval on the merge base (or a prior PR) before expanding or activating the " +
|
|
209
|
+
"scoped xBRIEF in the implementation change set. Editing the active xBRIEF alone " +
|
|
210
|
+
"does not authorize new paths (#3145 / #3205). See content/docs/scope-provenance.md.");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Parse + lightly validate an approved-scope JSON blob (base-ref `git show` or disk).
|
|
214
|
+
* Returns null when schema fields required for authorization are missing/malformed.
|
|
215
|
+
*/
|
|
216
|
+
export function parseApprovedScopeRecordRaw(raw) {
|
|
217
|
+
try {
|
|
218
|
+
const data = JSON.parse(raw);
|
|
219
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
220
|
+
return null;
|
|
221
|
+
const rec = data;
|
|
222
|
+
if (rec.schemaVersion !== undefined && rec.schemaVersion !== 1)
|
|
223
|
+
return null;
|
|
224
|
+
if (typeof rec.planId !== "string" || rec.planId.trim().length === 0)
|
|
225
|
+
return null;
|
|
226
|
+
if (typeof rec.xbriefRelPath !== "string" || rec.xbriefRelPath.trim().length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
if (typeof rec.fileScopeDigest !== "string" || rec.fileScopeDigest.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (!Array.isArray(rec.fileScope))
|
|
233
|
+
return null;
|
|
234
|
+
// Digest must match the recorded path list — never trust a forged digest alone (#3205 Greptile).
|
|
235
|
+
const scopePaths = rec.fileScope.filter((x) => typeof x === "string");
|
|
236
|
+
const expected = computeFileScopeDigest(scopePaths);
|
|
237
|
+
if (rec.fileScopeDigest !== expected)
|
|
238
|
+
return null;
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* True when the merge-base approved-scope record authorizes the current scope and
|
|
247
|
+
* the current disk record is semantically unchanged from that base authority (#3205).
|
|
248
|
+
*
|
|
249
|
+
* Authority comes from the approval record on the base, not from whether the active
|
|
250
|
+
* xBRIEF path existed on the base (pending→active is the normal first activation).
|
|
251
|
+
*/
|
|
252
|
+
export function baseApprovalAuthorizesCurrent(input) {
|
|
253
|
+
if (input.baseRef === null || input.baseRef === "")
|
|
254
|
+
return false;
|
|
255
|
+
const baseRaw = readRepoFileAtRef(input.projectRoot, input.baseRef, input.approvalRecordRel);
|
|
256
|
+
if (baseRaw === null)
|
|
257
|
+
return false;
|
|
258
|
+
const baseRec = parseApprovedScopeRecordRaw(baseRaw);
|
|
259
|
+
if (baseRec === null)
|
|
260
|
+
return false;
|
|
261
|
+
if (!isHumanApprovalStamp(baseRec.humanApproval))
|
|
262
|
+
return false;
|
|
263
|
+
if (baseRec.planId !== input.planId)
|
|
264
|
+
return false;
|
|
265
|
+
if (normalizeRepoRelPath(baseRec.xbriefRelPath) !== normalizeRepoRelPath(input.xbriefRelPath)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
// Base record must authorize the *current* file_scope (digest match).
|
|
269
|
+
if (baseRec.fileScopeDigest !== input.currentDigest)
|
|
270
|
+
return false;
|
|
271
|
+
// Current on-disk/injected record must not diverge from base authority fields.
|
|
272
|
+
if (input.currentApproved.fileScopeDigest !== baseRec.fileScopeDigest)
|
|
273
|
+
return false;
|
|
274
|
+
if (input.currentApproved.planId !== baseRec.planId)
|
|
275
|
+
return false;
|
|
276
|
+
if (normalizeRepoRelPath(input.currentApproved.xbriefRelPath) !==
|
|
277
|
+
normalizeRepoRelPath(baseRec.xbriefRelPath)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (!isHumanApprovalStamp(input.currentApproved.humanApproval))
|
|
281
|
+
return false;
|
|
282
|
+
return true;
|
|
209
283
|
}
|
|
210
284
|
function configError(message) {
|
|
211
285
|
return { exitCode: 2, findings: [], message };
|
|
@@ -250,13 +324,42 @@ export function evaluateOneScopeProvenance(input) {
|
|
|
250
324
|
isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
251
325
|
return null;
|
|
252
326
|
}
|
|
327
|
+
// Matching digest without human origin: empty-scope body edits may soft-warn
|
|
328
|
+
// via the missing-digest path only when no usable approval; agent/malformed
|
|
329
|
+
// stamps must not authorize non-empty scopes (#3205).
|
|
330
|
+
if (input.approved.fileScopeDigest === currentDigest) {
|
|
331
|
+
if (currentScope.length === 0) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
if (!isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
335
|
+
return {
|
|
336
|
+
xbriefRelPath: input.xbriefRelPath,
|
|
337
|
+
planId,
|
|
338
|
+
kind: "active-xbrief-modified-without-digest",
|
|
339
|
+
expandedPaths: currentScope,
|
|
340
|
+
detail: "active xBRIEF modified with a non-human (agent/missing) approved-scope stamp; " +
|
|
341
|
+
"only humanApproval stamps authorize non-empty file_scope",
|
|
342
|
+
remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
|
|
343
|
+
"<xbrief-path> --actor <you>` (#3145 / #3205).",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
253
347
|
const expanded = scopeExpansion(input.approved.fileScope, currentScope);
|
|
254
348
|
if (expanded.length === 0) {
|
|
255
|
-
// Scope shrink or
|
|
256
|
-
|
|
257
|
-
|
|
349
|
+
// Scope shrink or digest noise without path expansion — OK for v1 when human-stamped.
|
|
350
|
+
// Non-empty current scope still requires human origin (agent shrink must not bypass #3205).
|
|
351
|
+
if (currentScope.length > 0 && !isHumanApprovalStamp(input.approved.humanApproval)) {
|
|
352
|
+
return {
|
|
353
|
+
xbriefRelPath: input.xbriefRelPath,
|
|
354
|
+
planId,
|
|
355
|
+
kind: "active-xbrief-modified-without-digest",
|
|
356
|
+
expandedPaths: currentScope,
|
|
357
|
+
detail: "active xBRIEF modified with a non-human approved-scope stamp (scope shrink/noise path); " +
|
|
358
|
+
"only humanApproval stamps authorize non-empty file_scope",
|
|
359
|
+
remediation: "Record a human-origin approval via `task scope:record-approved-scope -- " +
|
|
360
|
+
"<xbrief-path> --actor <you>` (#3145 / #3205).",
|
|
361
|
+
};
|
|
258
362
|
}
|
|
259
|
-
// Digest mismatch without path expansion (reorder/noise) — still OK for v1
|
|
260
363
|
return null;
|
|
261
364
|
}
|
|
262
365
|
// Expansion without renewed human approval = self-authorization
|
|
@@ -303,10 +406,18 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
303
406
|
if (baseRef === undefined || baseRef === "" || baseRef === "HEAD") {
|
|
304
407
|
const resolved = resolveDefaultBaseRef(root);
|
|
305
408
|
if (resolved === null) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
409
|
+
// Greenfield / single-commit consumer trees often have no origin/* and no
|
|
410
|
+
// default-branch ref yet. Fail closed only when the caller demanded an
|
|
411
|
+
// explicit --base-ref; otherwise soft-skip (same posture as non-git trees)
|
|
412
|
+
// so verify:scope-provenance does not brick `task check` on init (#3205 smoke).
|
|
413
|
+
return {
|
|
414
|
+
exitCode: 0,
|
|
415
|
+
findings: [],
|
|
416
|
+
message: "verify_scope_provenance: skipped -- no merge-base ref found " +
|
|
417
|
+
"(origin/master|main, DEFT_BASE_REF, or GITHUB_BASE_REF). " +
|
|
418
|
+
"Fetch the default branch or pass --base-ref <ref> before enforcing " +
|
|
419
|
+
"PR scope expansion (#3145 / #3205).",
|
|
420
|
+
};
|
|
310
421
|
}
|
|
311
422
|
baseRef = resolved;
|
|
312
423
|
}
|
|
@@ -412,38 +523,36 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
412
523
|
return (n.includes("/approved-scope/") &&
|
|
413
524
|
(n.endsWith(`/${safe}.json`) || n.endsWith(`${safe}.json`)));
|
|
414
525
|
});
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
526
|
+
// Disk-only / concurrent-rewrite inference (#3205):
|
|
527
|
+
// Authority is the *approval record on the merge base*, not whether the
|
|
528
|
+
// active xBRIEF path existed there. pending→active leaves the active path
|
|
529
|
+
// absent on base; treating that as an approval rewrite is a false positive.
|
|
530
|
+
// Fail closed when base approval is missing, malformed, agent-stamped,
|
|
531
|
+
// path/plan/digest mismatched, or the current record diverged from base.
|
|
532
|
+
// Same-PR git changes still hard-fail via approvalInGitChange.
|
|
419
533
|
let approvalDiskOnly = false;
|
|
420
534
|
if (modified &&
|
|
421
535
|
approved !== null &&
|
|
422
536
|
renewed === null &&
|
|
423
537
|
approvalRecordRel !== null &&
|
|
538
|
+
planId !== null &&
|
|
424
539
|
!approvalInGitChange &&
|
|
425
540
|
existsSync(join(root, approvalRecordRel)) &&
|
|
426
541
|
isHumanApprovalStamp(approved.humanApproval)) {
|
|
427
542
|
const currentDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(payload)));
|
|
428
543
|
if (approved.fileScopeDigest === currentDigest) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
const baseAuthorizes = baseApprovalAuthorizesCurrent({
|
|
545
|
+
projectRoot: root,
|
|
546
|
+
baseRef: discoveryBaseRef,
|
|
547
|
+
approvalRecordRel,
|
|
548
|
+
planId,
|
|
549
|
+
xbriefRelPath: rel,
|
|
550
|
+
currentDigest,
|
|
551
|
+
currentApproved: approved,
|
|
552
|
+
});
|
|
553
|
+
if (!baseAuthorizes) {
|
|
432
554
|
approvalDiskOnly = true;
|
|
433
555
|
}
|
|
434
|
-
else {
|
|
435
|
-
try {
|
|
436
|
-
const basePayload = JSON.parse(baseRaw);
|
|
437
|
-
const baseDigest = computeFileScopeDigest(normalizeFileScope(extractFileScope(basePayload)));
|
|
438
|
-
// Only concurrent-rewrite when file-scope actually grew/changed.
|
|
439
|
-
if (baseDigest !== currentDigest) {
|
|
440
|
-
approvalDiskOnly = true;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
approvalDiskOnly = true;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
556
|
}
|
|
448
557
|
}
|
|
449
558
|
const approvalRecordRewritten = approvalInGitChange || approvalDiskOnly;
|
|
@@ -463,8 +572,9 @@ export function evaluateScopeProvenance(projectRoot, options = {}) {
|
|
|
463
572
|
expandedPaths: currentScope,
|
|
464
573
|
detail: "approved-scope record rewritten in the same change set as the active xBRIEF; " +
|
|
465
574
|
"cannot self-authorize via concurrent approval rewrite",
|
|
466
|
-
remediation: "
|
|
467
|
-
"
|
|
575
|
+
remediation: "Commit human approval via `task scope:record-approved-scope` on the merge base " +
|
|
576
|
+
"(or a prior PR), then activate/expand without rewriting the approval in this " +
|
|
577
|
+
"change set. Same-PR approval rewrites do not authorize expansion (#3145 / #3205).",
|
|
468
578
|
});
|
|
469
579
|
continue;
|
|
470
580
|
}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* scope-provenance package surface (#3145).
|
|
3
3
|
*/
|
|
4
4
|
export { APPROVED_SCOPE_DIR, type ApprovedScopeRecord, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
|
|
5
|
-
export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
|
|
5
|
+
export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, type ScopeProvenanceFinding, type ScopeProvenanceOptions, type ScopeProvenanceResult, type ScopeProvenanceViolationKind, } from "./evaluate.js";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* scope-provenance package surface (#3145).
|
|
3
3
|
*/
|
|
4
4
|
export { APPROVED_SCOPE_DIR, approvedScopeDir, approvedScopeRecordPath, buildApprovedScopeRecord, computeFileScopeDigest, computeTextDigest, extractFileScope, extractPlanId, isHumanApprovalStamp, listApprovedScopeRecords, normalizeFileScope, readApprovedScopeRecord, scopeExpansion, writeApprovedScopeRecord, } from "./digest.js";
|
|
5
|
-
export { evaluateOneScopeProvenance, evaluateScopeProvenance, resolveDefaultBaseRef, } from "./evaluate.js";
|
|
5
|
+
export { baseApprovalAuthorizesCurrent, evaluateOneScopeProvenance, evaluateScopeProvenance, parseApprovedScopeRecordRaw, resolveDefaultBaseRef, } from "./evaluate.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type HostContentSurfaceSeams } from "../platform/host-content-surface.js";
|
|
1
2
|
import { type EnvironmentContext } from "../platform/shell-context.js";
|
|
3
|
+
import { type CeremonyDialInputs, type CeremonyDialSelection, type ProvisionalCeremonyEstimateHints } from "../policy/ceremony-dial.js";
|
|
2
4
|
import { type ProbeScmReadinessOptions, type ScmReadinessReport } from "../scm/readiness.js";
|
|
3
5
|
import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
|
|
4
6
|
import type { GitRunner } from "./git.js";
|
|
@@ -15,7 +17,7 @@ export declare const SESSION_CEREMONY_TIERS: readonly ["cold", "rearm"];
|
|
|
15
17
|
export type SessionCeremonyTier = (typeof SESSION_CEREMONY_TIERS)[number];
|
|
16
18
|
export declare const COLD_CEREMONY_TIER: SessionCeremonyTier;
|
|
17
19
|
export declare const REARM_CEREMONY_TIER: SessionCeremonyTier;
|
|
18
|
-
export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome"];
|
|
20
|
+
export declare const QUICK_STEPS: readonly ["alignment", "branch_policy", "triage_welcome", "verify_tools"];
|
|
19
21
|
export declare const GATED_STEPS: readonly ["agent_hooks", "doctor", "cache_fresh"];
|
|
20
22
|
export type GatedStepName = (typeof GATED_STEPS)[number];
|
|
21
23
|
/** Env opt-in for optional session:start network (release probe + triage cache hydrate) (#2991). */
|
|
@@ -69,6 +71,11 @@ export interface SessionStartOptions {
|
|
|
69
71
|
};
|
|
70
72
|
readonly resolveUserMd?: (projectRoot: string) => ResolveUserMdResult;
|
|
71
73
|
readonly probeEnvironment?: () => EnvironmentContext;
|
|
74
|
+
/**
|
|
75
|
+
* #3162: host content-surface class + managed-section drift seams (tests inject).
|
|
76
|
+
* Fail-open advisory only — never blocks session:start.
|
|
77
|
+
*/
|
|
78
|
+
readonly hostContentSurfaceSeams?: HostContentSurfaceSeams;
|
|
72
79
|
readonly probeReleaseAvailability?: (projectRoot: string, options: ReleaseAvailabilityProbeOptions) => {
|
|
73
80
|
lines: readonly string[];
|
|
74
81
|
};
|
|
@@ -94,6 +101,22 @@ export interface SessionStartOptions {
|
|
|
94
101
|
* network is enabled. Inject in tests.
|
|
95
102
|
*/
|
|
96
103
|
readonly probeScm?: (options: ProbeScmReadinessOptions) => ScmReadinessReport;
|
|
104
|
+
/**
|
|
105
|
+
* #3214: ceremony dial inputs (task size × model tier × project shape).
|
|
106
|
+
* Missing fields are filled by the headless provisional classifier
|
|
107
|
+
* (env / verb / file-scope / deposit layout) — no plan-item effort (#1581).
|
|
108
|
+
*/
|
|
109
|
+
readonly ceremonyDialInputs?: CeremonyDialInputs;
|
|
110
|
+
/**
|
|
111
|
+
* #3214: optional intake hints for provisional size (prompt/verb/files).
|
|
112
|
+
* Vanilla deposit session:start runs provisional fill without policy opt-in.
|
|
113
|
+
*/
|
|
114
|
+
readonly ceremonyDialHints?: Omit<ProvisionalCeremonyEstimateHints, "projectRoot" | "env">;
|
|
115
|
+
/**
|
|
116
|
+
* #3214: optional pre-resolved dial (tests). When omitted, resolveCeremonyDial
|
|
117
|
+
* loads plan.policy.ceremonyDial and applies inputs (after provisional fill).
|
|
118
|
+
*/
|
|
119
|
+
readonly ceremonyDial?: CeremonyDialSelection;
|
|
97
120
|
}
|
|
98
121
|
/** Format preferred `session:start` recovery command for cold vs re-arm (#2992). */
|
|
99
122
|
export declare function formatSessionStartRecoveryCommand(tier?: SessionCeremonyTier): string;
|