@deftai/directive-core 0.98.1 → 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.
Files changed (55) hide show
  1. package/dist/authz/classify.js +265 -73
  2. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  3. package/dist/consumer-check-contract/evaluate.js +188 -3
  4. package/dist/consumer-check-contract/index.d.ts +1 -1
  5. package/dist/consumer-check-contract/index.js +1 -1
  6. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  7. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  8. package/dist/decision/index.d.ts +17 -0
  9. package/dist/decision/index.js +35 -0
  10. package/dist/decision/list.d.ts +47 -0
  11. package/dist/decision/list.js +250 -0
  12. package/dist/decision/schema.d.ts +88 -0
  13. package/dist/decision/schema.js +293 -0
  14. package/dist/decision/write.d.ts +82 -0
  15. package/dist/decision/write.js +427 -0
  16. package/dist/eval/report.d.ts +29 -0
  17. package/dist/eval/report.js +69 -0
  18. package/dist/eval/run.d.ts +9 -0
  19. package/dist/eval/run.js +40 -4
  20. package/dist/eval/version-pin.d.ts +99 -0
  21. package/dist/eval/version-pin.js +181 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/platform/host-content-surface.d.ts +74 -0
  25. package/dist/platform/host-content-surface.js +214 -0
  26. package/dist/platform/index.d.ts +1 -0
  27. package/dist/platform/index.js +1 -0
  28. package/dist/policy/ceremony-dial.d.ts +233 -0
  29. package/dist/policy/ceremony-dial.js +829 -0
  30. package/dist/policy/deft-directive-disable.js +12 -2
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-merge-readiness/evaluate.js +10 -0
  34. package/dist/pr-merge-readiness/mergeability.js +5 -0
  35. package/dist/pr-merge-readiness/output.js +2 -0
  36. package/dist/pr-merge-readiness/parse.js +4 -0
  37. package/dist/pr-merge-readiness/types.d.ts +6 -0
  38. package/dist/scope/effort-activate-gate.d.ts +28 -0
  39. package/dist/scope/effort-activate-gate.js +64 -0
  40. package/dist/scope/index.d.ts +1 -0
  41. package/dist/scope/index.js +1 -0
  42. package/dist/scope/transition.js +8 -0
  43. package/dist/session/session-start.d.ts +24 -1
  44. package/dist/session/session-start.js +183 -26
  45. package/dist/swarm/index.d.ts +2 -0
  46. package/dist/swarm/index.js +2 -0
  47. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  48. package/dist/swarm/pre-dispatch-cli.js +143 -0
  49. package/dist/swarm/pre-dispatch.d.ts +87 -0
  50. package/dist/swarm/pre-dispatch.js +373 -0
  51. package/dist/vbrief-activate/activate.js +6 -0
  52. package/dist/vbrief-validate/constants.d.ts +2 -0
  53. package/dist/vbrief-validate/constants.js +2 -0
  54. package/dist/vbrief-validate/schema.js +4 -1
  55. 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
- return statSync(path).isFile();
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;
@@ -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";
@@ -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
@@ -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";
@@ -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";
@@ -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({
@@ -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;