@deftai/directive-core 0.91.0 → 0.92.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 (46) hide show
  1. package/dist/cache/fetch.js +3 -2
  2. package/dist/cache/io.d.ts +13 -2
  3. package/dist/cache/io.js +36 -7
  4. package/dist/cache/operations.js +5 -3
  5. package/dist/doctor/main.js +37 -0
  6. package/dist/hooks/dispatcher.d.ts +6 -1
  7. package/dist/hooks/dispatcher.js +30 -0
  8. package/dist/init-deposit/gitignore.js +3 -0
  9. package/dist/init-deposit/hygiene.d.ts +43 -1
  10. package/dist/init-deposit/hygiene.js +112 -10
  11. package/dist/init-deposit/scaffold.js +3 -1
  12. package/dist/orchestration/probe-session.d.ts +5 -1
  13. package/dist/orchestration/probe-session.js +24 -13
  14. package/dist/platform/agents-md.js +1 -1
  15. package/dist/policy/deft-directive-disable.d.ts +86 -0
  16. package/dist/policy/deft-directive-disable.js +167 -0
  17. package/dist/policy/delivery-branch.d.ts +33 -0
  18. package/dist/policy/delivery-branch.js +124 -0
  19. package/dist/policy/index.d.ts +2 -0
  20. package/dist/policy/index.js +17 -1
  21. package/dist/scope/brief-io.d.ts +3 -1
  22. package/dist/scope/brief-io.js +5 -3
  23. package/dist/scope/delivery-evidence.d.ts +112 -0
  24. package/dist/scope/delivery-evidence.js +419 -0
  25. package/dist/scope/index.d.ts +1 -0
  26. package/dist/scope/index.js +1 -0
  27. package/dist/scope/main.d.ts +5 -0
  28. package/dist/scope/main.js +98 -3
  29. package/dist/scope/registry-artifact-sync.js +4 -1
  30. package/dist/scope/transition.d.ts +11 -1
  31. package/dist/scope/transition.js +30 -4
  32. package/dist/session/ritual-sentinel.js +21 -12
  33. package/dist/session/session-start-hook.d.ts +3 -0
  34. package/dist/session/session-start-hook.js +21 -0
  35. package/dist/session/session-start.js +31 -0
  36. package/dist/swarm/complete-cohort.d.ts +20 -1
  37. package/dist/swarm/complete-cohort.js +56 -9
  38. package/dist/swarm/finalize-cohort-cli.js +9 -0
  39. package/dist/swarm/finalize-cohort.d.ts +8 -0
  40. package/dist/swarm/finalize-cohort.js +217 -21
  41. package/dist/user-config/experimental-rules.d.ts +43 -0
  42. package/dist/user-config/experimental-rules.js +162 -0
  43. package/dist/user-config/index.d.ts +1 -0
  44. package/dist/user-config/index.js +1 -0
  45. package/dist/verify-source/contained-writes.js +1 -1
  46. package/package.json +3 -3
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Temporary test/local kill-switch for Directive enforcement (#3039).
3
+ *
4
+ * Presence of root `.deft-directive-disable` means Directive **enforcement** is
5
+ * OFF (hooks, session ritual, automation) while the deposit may remain — **only
6
+ * when the file is not tracked by git**. A committed flag is misconfig: doctor
7
+ * warns and enforcement stays ON (repository-controlled content must not disable
8
+ * hooks for downstream clones).
9
+ *
10
+ * Distinct from `.no-deft-directive` (#2926 permanent opt-out): flag+deposit is
11
+ * **not** inconsistent here.
12
+ *
13
+ * Product choices (v1):
14
+ * - Flag is **root-only** (workspace root the tool opened).
15
+ * - Flag **must be gitignored** (canonical baseline); committed flag → doctor
16
+ * warns and does **not** short-circuit enforcement.
17
+ * - Deposit presence is **OK**.
18
+ * - Full re-enable requires: delete the file **and** start a **new** agent session.
19
+ * - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
20
+ */
21
+ /** Canonical root-only filename (lowercase). Presence = candidate flag. */
22
+ export declare const DEFT_DIRECTIVE_DISABLE_FLAG_NAME = ".deft-directive-disable";
23
+ /** Gitignore entry the deposit must ensure (same as flag name). */
24
+ export declare const DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE = ".deft-directive-disable";
25
+ /**
26
+ * Canonical recovery message when Directive is disabled by the test kill-switch.
27
+ * Surfaces (doctor, agent, CLI, hooks) share this wording.
28
+ */
29
+ export declare const DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE: string;
30
+ /** One-line summary for log lines and short CLI output. */
31
+ export declare const DEFT_DIRECTIVE_DISABLE_ONE_LINE = "Directive disabled via `.deft-directive-disable` (test/local kill-switch; deposit OK)";
32
+ /** Doctor/status label when the kill-switch is active (local / untracked). */
33
+ export declare const DEFT_DIRECTIVE_DISABLE_STATUS: "disabled-test-kill-switch";
34
+ /**
35
+ * Warning when the kill-switch file is tracked by git (should be gitignored).
36
+ * Tracked flags do **not** disable enforcement.
37
+ */
38
+ export declare const DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING = "Misconfig: `.deft-directive-disable` is tracked by git. The flag must be gitignored (local kill-switch only). Untrack it and ensure `.gitignore` covers the path. Enforcement is NOT disabled while the flag is tracked.";
39
+ export interface DeftDirectiveDisableSeams {
40
+ readonly isFile?: (path: string) => boolean;
41
+ readonly isDir?: (path: string) => boolean;
42
+ /** Return true when `git ls-files` lists the flag (tracked). */
43
+ readonly isGitTracked?: (projectRoot: string, relPath: string) => boolean;
44
+ /** Skip tracked probe + cache (tests / doctor full re-check). */
45
+ readonly skipTrackedCache?: boolean;
46
+ }
47
+ export interface DeftDirectiveDisableState {
48
+ readonly present: boolean;
49
+ readonly flagPath: string;
50
+ readonly depositPresent: boolean;
51
+ /**
52
+ * True when the flag file is tracked by git (misconfig — should be gitignored).
53
+ * Always false when the flag is absent or when the probe cannot run.
54
+ */
55
+ readonly trackedByGit: boolean;
56
+ /**
57
+ * True when the kill-switch is **active** for enforcement short-circuit:
58
+ * present and **not** tracked by git.
59
+ */
60
+ readonly active: boolean;
61
+ }
62
+ /** Absolute path to the root kill-switch flag file. */
63
+ export declare function deftDirectiveDisableFlagPath(projectRoot: string): string;
64
+ /**
65
+ * Detect root `.deft-directive-disable`. Presence is file-existence only
66
+ * (empty or short comment OK). Deposit presence is reported but never treated
67
+ * as inconsistent (#3039 vs #2926).
68
+ *
69
+ * Enforcement short-circuit uses `state.active` (present && !trackedByGit).
70
+ */
71
+ export declare function detectDeftDirectiveDisable(projectRoot: string, seams?: DeftDirectiveDisableSeams): DeftDirectiveDisableState;
72
+ /**
73
+ * True when the local (untracked) kill-switch is active for enforcement
74
+ * short-circuit. Tracked flags return false so hooks stay enforced.
75
+ */
76
+ export declare function isDeftDirectiveDisableActive(projectRoot: string, seams?: DeftDirectiveDisableSeams): boolean;
77
+ /**
78
+ * Full operator-facing message for the kill-switch, optionally combined with
79
+ * permanent opt-out when both flags are present.
80
+ */
81
+ export declare function formatDeftDirectiveDisableMessage(options?: {
82
+ readonly permanentOptOutAlsoPresent?: boolean;
83
+ readonly trackedByGit?: boolean;
84
+ readonly oneLine?: boolean;
85
+ }): string;
86
+ //# sourceMappingURL=deft-directive-disable.d.ts.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Temporary test/local kill-switch for Directive enforcement (#3039).
3
+ *
4
+ * Presence of root `.deft-directive-disable` means Directive **enforcement** is
5
+ * OFF (hooks, session ritual, automation) while the deposit may remain — **only
6
+ * when the file is not tracked by git**. A committed flag is misconfig: doctor
7
+ * warns and enforcement stays ON (repository-controlled content must not disable
8
+ * hooks for downstream clones).
9
+ *
10
+ * Distinct from `.no-deft-directive` (#2926 permanent opt-out): flag+deposit is
11
+ * **not** inconsistent here.
12
+ *
13
+ * Product choices (v1):
14
+ * - Flag is **root-only** (workspace root the tool opened).
15
+ * - Flag **must be gitignored** (canonical baseline); committed flag → doctor
16
+ * warns and does **not** short-circuit enforcement.
17
+ * - Deposit presence is **OK**.
18
+ * - Full re-enable requires: delete the file **and** start a **new** agent session.
19
+ * - Precedence: active kill-switch first, then `.no-deft-directive`, else normal.
20
+ */
21
+ import { execFileSync } from "node:child_process";
22
+ import { statSync } from "node:fs";
23
+ import { join, resolve } from "node:path";
24
+ import { CANONICAL_INSTALL_ROOT } from "../init-deposit/constants.js";
25
+ /** Canonical root-only filename (lowercase). Presence = candidate flag. */
26
+ export const DEFT_DIRECTIVE_DISABLE_FLAG_NAME = ".deft-directive-disable";
27
+ /** Gitignore entry the deposit must ensure (same as flag name). */
28
+ export const DEFT_DIRECTIVE_DISABLE_GITIGNORE_LINE = DEFT_DIRECTIVE_DISABLE_FLAG_NAME;
29
+ /**
30
+ * Canonical recovery message when Directive is disabled by the test kill-switch.
31
+ * Surfaces (doctor, agent, CLI, hooks) share this wording.
32
+ */
33
+ export const DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE = "Directive is DISABLED for this project via root `.deft-directive-disable` (test/local kill-switch).\n" +
34
+ "Deposit may still be present; enforcement (hooks, session ritual, automation) will not run.\n" +
35
+ "\n" +
36
+ "To fully re-enable Directive:\n" +
37
+ " 1. Delete the file: rm .deft-directive-disable (or equivalent)\n" +
38
+ " 2. Start a NEW agent session (reload AGENTS / host skills / hooks)\n" +
39
+ "Until both are done, Directive is not fully operational.";
40
+ /** One-line summary for log lines and short CLI output. */
41
+ export const DEFT_DIRECTIVE_DISABLE_ONE_LINE = "Directive disabled via `.deft-directive-disable` (test/local kill-switch; deposit OK)";
42
+ /** Doctor/status label when the kill-switch is active (local / untracked). */
43
+ export const DEFT_DIRECTIVE_DISABLE_STATUS = "disabled-test-kill-switch";
44
+ /**
45
+ * Warning when the kill-switch file is tracked by git (should be gitignored).
46
+ * Tracked flags do **not** disable enforcement.
47
+ */
48
+ export const DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING = "Misconfig: `.deft-directive-disable` is tracked by git. The flag must be gitignored (local kill-switch only). Untrack it and ensure `.gitignore` covers the path. Enforcement is NOT disabled while the flag is tracked.";
49
+ /** Cap git probe so hot-path hooks never hang on a slow VCS call. */
50
+ const GIT_TRACKED_PROBE_TIMEOUT_MS = 1500;
51
+ /** Process-local cache: avoid re-spawning git on every PreToolUse while the flag is present. */
52
+ const trackedProbeCache = new Map();
53
+ const TRACKED_PROBE_CACHE_TTL_MS = 30_000;
54
+ function defaultIsFile(path) {
55
+ try {
56
+ return statSync(path).isFile();
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ function defaultIsDir(path) {
63
+ try {
64
+ return statSync(path).isDirectory();
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function defaultIsGitTracked(projectRoot, relPath) {
71
+ const cacheKey = `${projectRoot}\0${relPath}`;
72
+ const now = Date.now();
73
+ const cached = trackedProbeCache.get(cacheKey);
74
+ if (cached !== undefined && now - cached.atMs < TRACKED_PROBE_CACHE_TTL_MS) {
75
+ return cached.tracked;
76
+ }
77
+ let tracked = false;
78
+ try {
79
+ const out = execFileSync("git", ["ls-files", "--", relPath], {
80
+ cwd: projectRoot,
81
+ encoding: "utf8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ timeout: GIT_TRACKED_PROBE_TIMEOUT_MS,
84
+ });
85
+ tracked = out.trim().length > 0;
86
+ }
87
+ catch {
88
+ tracked = false;
89
+ }
90
+ trackedProbeCache.set(cacheKey, { tracked, atMs: now });
91
+ return tracked;
92
+ }
93
+ /** Absolute path to the root kill-switch flag file. */
94
+ export function deftDirectiveDisableFlagPath(projectRoot) {
95
+ return resolve(projectRoot, DEFT_DIRECTIVE_DISABLE_FLAG_NAME);
96
+ }
97
+ /**
98
+ * Detect root `.deft-directive-disable`. Presence is file-existence only
99
+ * (empty or short comment OK). Deposit presence is reported but never treated
100
+ * as inconsistent (#3039 vs #2926).
101
+ *
102
+ * Enforcement short-circuit uses `state.active` (present && !trackedByGit).
103
+ */
104
+ export function detectDeftDirectiveDisable(projectRoot, seams = {}) {
105
+ const flagPath = deftDirectiveDisableFlagPath(projectRoot);
106
+ const isFile = seams.isFile ?? defaultIsFile;
107
+ const isDir = seams.isDir ?? defaultIsDir;
108
+ const isGitTracked = seams.isGitTracked ?? defaultIsGitTracked;
109
+ const present = isFile(flagPath);
110
+ const depositPresent = isDir(join(projectRoot, CANONICAL_INSTALL_ROOT));
111
+ let trackedByGit = false;
112
+ if (present) {
113
+ if (seams.skipTrackedCache && seams.isGitTracked === undefined) {
114
+ // Fresh probe without reading process cache (doctor / one-shot CLI).
115
+ try {
116
+ const out = execFileSync("git", ["ls-files", "--", DEFT_DIRECTIVE_DISABLE_FLAG_NAME], {
117
+ cwd: projectRoot,
118
+ encoding: "utf8",
119
+ stdio: ["ignore", "pipe", "ignore"],
120
+ timeout: GIT_TRACKED_PROBE_TIMEOUT_MS,
121
+ });
122
+ trackedByGit = out.trim().length > 0;
123
+ }
124
+ catch {
125
+ trackedByGit = false;
126
+ }
127
+ }
128
+ else {
129
+ trackedByGit = isGitTracked(projectRoot, DEFT_DIRECTIVE_DISABLE_FLAG_NAME);
130
+ }
131
+ }
132
+ return {
133
+ present,
134
+ flagPath,
135
+ depositPresent,
136
+ trackedByGit,
137
+ active: present && !trackedByGit,
138
+ };
139
+ }
140
+ /**
141
+ * True when the local (untracked) kill-switch is active for enforcement
142
+ * short-circuit. Tracked flags return false so hooks stay enforced.
143
+ */
144
+ export function isDeftDirectiveDisableActive(projectRoot, seams = {}) {
145
+ return detectDeftDirectiveDisable(projectRoot, seams).active;
146
+ }
147
+ /**
148
+ * Full operator-facing message for the kill-switch, optionally combined with
149
+ * permanent opt-out when both flags are present.
150
+ */
151
+ export function formatDeftDirectiveDisableMessage(options = {}) {
152
+ const parts = [];
153
+ if (options.oneLine) {
154
+ parts.push(DEFT_DIRECTIVE_DISABLE_ONE_LINE);
155
+ }
156
+ else {
157
+ parts.push(DEFT_DIRECTIVE_DISABLE_RECOVERY_MESSAGE);
158
+ }
159
+ if (options.permanentOptOutAlsoPresent) {
160
+ parts.push("Also present: root `.no-deft-directive` (permanent opt-out). Install/update fail-closed semantics still apply for that flag.");
161
+ }
162
+ if (options.trackedByGit) {
163
+ parts.push(DEFT_DIRECTIVE_DISABLE_TRACKED_WARNING);
164
+ }
165
+ return parts.join("\n\n");
166
+ }
167
+ //# sourceMappingURL=deft-directive-disable.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed plan.policy.deliveryBranch (#3041).
3
+ *
4
+ * Distinct from swarm/PR `baseBranch` (integration target for PRs / finalize
5
+ * sweep PRs). deliveryBranch is where shipped work must land for a delivered
6
+ * completion disposition.
7
+ */
8
+ import { type GitRunner } from "../session/git.js";
9
+ export declare const FIELD_DELIVERY_BRANCH = "plan.policy.deliveryBranch";
10
+ export declare const FIELD_DELIVERY_BRANCH_CLI_ALIAS = "deliveryBranch";
11
+ /** Framework fallback when neither policy nor git default can be resolved. */
12
+ export declare const DEFAULT_DELIVERY_BRANCH_FALLBACK = "master";
13
+ export type DeliveryBranchSource = "typed" | "git-default" | "default-fallback" | "default-on-error";
14
+ export interface DeliveryBranchResult {
15
+ readonly branch: string;
16
+ readonly source: DeliveryBranchSource;
17
+ readonly error: string | null;
18
+ }
19
+ /**
20
+ * Resolve the project's delivery branch (#3041).
21
+ *
22
+ * Order: typed plan.policy.deliveryBranch → git remote default → local main/master → "master".
23
+ */
24
+ export declare function resolveDeliveryBranch(projectRoot: string, runGit?: GitRunner): DeliveryBranchResult;
25
+ export interface DeliveryBranchPolicyField {
26
+ readonly name: string;
27
+ readonly current: string;
28
+ readonly default: string;
29
+ readonly source: string;
30
+ }
31
+ /** Inspector row for `task policy:show --field=deliveryBranch` (#3041). */
32
+ export declare function inspectDeliveryBranch(_data: Record<string, unknown> | null, projectRoot?: string): DeliveryBranchPolicyField;
33
+ //# sourceMappingURL=delivery-branch.d.ts.map
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Typed plan.policy.deliveryBranch (#3041).
3
+ *
4
+ * Distinct from swarm/PR `baseBranch` (integration target for PRs / finalize
5
+ * sweep PRs). deliveryBranch is where shipped work must land for a delivered
6
+ * completion disposition.
7
+ */
8
+ import { defaultGitRunner } from "../session/git.js";
9
+ import { readPlanPolicy } from "./plan-extensions.js";
10
+ import { loadProjectDefinition } from "./resolve.js";
11
+ export const FIELD_DELIVERY_BRANCH = "plan.policy.deliveryBranch";
12
+ export const FIELD_DELIVERY_BRANCH_CLI_ALIAS = "deliveryBranch";
13
+ /** Framework fallback when neither policy nor git default can be resolved. */
14
+ export const DEFAULT_DELIVERY_BRANCH_FALLBACK = "master";
15
+ function defaultBranchCandidates(projectRoot, runGit) {
16
+ const sym = runGit(projectRoot, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
17
+ if (sym.code === 0 && sym.stdout) {
18
+ const trimmed = sym.stdout.trim();
19
+ // origin/main → main
20
+ const parts = trimmed.split("/");
21
+ const name = (parts.slice(1).join("/") || parts[0] || "").trim();
22
+ if (name.length > 0) {
23
+ return [name];
24
+ }
25
+ }
26
+ const candidates = [];
27
+ for (const branch of ["main", "master"]) {
28
+ const check = runGit(projectRoot, [
29
+ "show-ref",
30
+ "--verify",
31
+ "--quiet",
32
+ `refs/remotes/origin/${branch}`,
33
+ ]);
34
+ if (check.code === 0) {
35
+ candidates.push(branch);
36
+ }
37
+ }
38
+ if (candidates.length === 0) {
39
+ for (const branch of ["main", "master"]) {
40
+ const local = runGit(projectRoot, [
41
+ "show-ref",
42
+ "--verify",
43
+ "--quiet",
44
+ `refs/heads/${branch}`,
45
+ ]);
46
+ if (local.code === 0) {
47
+ candidates.push(branch);
48
+ }
49
+ }
50
+ }
51
+ return candidates;
52
+ }
53
+ /**
54
+ * Resolve the project's delivery branch (#3041).
55
+ *
56
+ * Order: typed plan.policy.deliveryBranch → git remote default → local main/master → "master".
57
+ */
58
+ export function resolveDeliveryBranch(projectRoot, runGit = defaultGitRunner) {
59
+ const [data, err] = loadProjectDefinition(projectRoot);
60
+ if (data === null) {
61
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
62
+ if (gitDefault !== undefined) {
63
+ return { branch: gitDefault, source: "git-default", error: err };
64
+ }
65
+ return {
66
+ branch: DEFAULT_DELIVERY_BRANCH_FALLBACK,
67
+ source: "default-fallback",
68
+ error: err,
69
+ };
70
+ }
71
+ const plan = data.plan;
72
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
73
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
74
+ return {
75
+ branch: gitDefault ?? DEFAULT_DELIVERY_BRANCH_FALLBACK,
76
+ source: gitDefault !== undefined ? "git-default" : "default-fallback",
77
+ error: "PROJECT-DEFINITION 'plan' is not an object",
78
+ };
79
+ }
80
+ const policyBlock = readPlanPolicy(plan);
81
+ if (typeof policyBlock === "object" &&
82
+ policyBlock !== null &&
83
+ !Array.isArray(policyBlock) &&
84
+ "deliveryBranch" in policyBlock) {
85
+ const raw = policyBlock.deliveryBranch;
86
+ if (typeof raw !== "string" || raw.trim().length === 0) {
87
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
88
+ return {
89
+ branch: gitDefault ?? DEFAULT_DELIVERY_BRANCH_FALLBACK,
90
+ source: "default-on-error",
91
+ error: `plan.policy.deliveryBranch must be a non-empty string; got ${typeof raw}`,
92
+ };
93
+ }
94
+ return { branch: raw.trim(), source: "typed", error: null };
95
+ }
96
+ const gitDefault = defaultBranchCandidates(projectRoot, runGit)[0];
97
+ if (gitDefault !== undefined) {
98
+ return { branch: gitDefault, source: "git-default", error: null };
99
+ }
100
+ return {
101
+ branch: DEFAULT_DELIVERY_BRANCH_FALLBACK,
102
+ source: "default-fallback",
103
+ error: null,
104
+ };
105
+ }
106
+ /** Inspector row for `task policy:show --field=deliveryBranch` (#3041). */
107
+ export function inspectDeliveryBranch(_data, projectRoot) {
108
+ if (projectRoot === undefined || projectRoot.length === 0) {
109
+ return {
110
+ name: FIELD_DELIVERY_BRANCH,
111
+ current: DEFAULT_DELIVERY_BRANCH_FALLBACK,
112
+ default: DEFAULT_DELIVERY_BRANCH_FALLBACK,
113
+ source: "default",
114
+ };
115
+ }
116
+ const resolved = resolveDeliveryBranch(projectRoot);
117
+ return {
118
+ name: FIELD_DELIVERY_BRANCH,
119
+ current: resolved.branch,
120
+ default: DEFAULT_DELIVERY_BRANCH_FALLBACK,
121
+ source: resolved.source,
122
+ };
123
+ }
124
+ //# sourceMappingURL=delivery-branch.js.map
@@ -2,6 +2,8 @@ export * from "./agents-md-advisory.js";
2
2
  export * from "./autonomy.js";
3
3
  export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
+ export * from "./deft-directive-disable.js";
6
+ export * from "./delivery-branch.js";
5
7
  export * from "./disclosure.js";
6
8
  export * from "./host-hooks.js";
7
9
  export * from "./hotfix-criteria.js";
@@ -1,3 +1,4 @@
1
+ import { FIELD_DELIVERY_BRANCH, FIELD_DELIVERY_BRANCH_CLI_ALIAS, inspectDeliveryBranch, } from "./delivery-branch.js";
1
2
  import { FIELD_HOST_HOOKS, FIELD_HOST_HOOKS_CLI_ALIAS, inspectHostHooks } from "./host-hooks.js";
2
3
  import { FIELD_HOTFIX_CRITERIA, FIELD_HOTFIX_CRITERIA_CLI_ALIAS, inspectHotfixCriteria, } from "./hotfix-criteria.js";
3
4
  import { readPlanPolicy } from "./plan-extensions.js";
@@ -12,6 +13,8 @@ export * from "./agents-md-advisory.js";
12
13
  export * from "./autonomy.js";
13
14
  export * from "./capacity.js";
14
15
  export * from "./decisions.js";
16
+ export * from "./deft-directive-disable.js";
17
+ export * from "./delivery-branch.js";
15
18
  export * from "./disclosure.js";
16
19
  export * from "./host-hooks.js";
17
20
  export * from "./hotfix-criteria.js";
@@ -38,6 +41,7 @@ export const FIELD_TRIAGE_RANKING_LABELS = "plan.policy.triageRankingLabels";
38
41
  export const FIELD_TRIAGE_AUTO_CLASSIFY = "plan.policy.triageAutoClassify";
39
42
  export const FIELD_TRIAGE_HOLD_MARKERS = "plan.policy.triageHoldMarkers";
40
43
  export const FIELD_SWARM_SUBAGENT_BACKEND = "plan.policy.swarmSubagentBackend";
44
+ // deliveryBranch also exported from delivery-branch.js via export *
41
45
  export const DEFAULT_SESSION_RITUAL_STALENESS_HOURS = 4;
42
46
  export const DEFAULT_TRIAGE_SCOPE_VALUE = [
43
47
  { rule: "all-open" },
@@ -296,6 +300,15 @@ function inspectHotfixCriteriaField(data) {
296
300
  source: field.source,
297
301
  };
298
302
  }
303
+ function inspectDeliveryBranchField(data, projectRoot) {
304
+ const field = inspectDeliveryBranch(data, projectRoot);
305
+ return {
306
+ name: field.name,
307
+ current: field.current,
308
+ default: field.default,
309
+ source: field.source,
310
+ };
311
+ }
299
312
  const REGISTERED_POLICIES = [
300
313
  inspectAllowDirectCommits,
301
314
  inspectWipCap,
@@ -308,6 +321,7 @@ const REGISTERED_POLICIES = [
308
321
  emptyIsTyped: true,
309
322
  }),
310
323
  inspectSwarmSubagentBackend,
324
+ inspectDeliveryBranchField,
311
325
  inspectHostHooksField,
312
326
  inspectStalenessTicklerField,
313
327
  inspectRuntimeAuthorityField,
@@ -337,7 +351,9 @@ export function inspectOnePolicy(name, projectRoot) {
337
351
  ? FIELD_REQUIRE_HUMAN_MERGE
338
352
  : name === FIELD_HOTFIX_CRITERIA_CLI_ALIAS
339
353
  ? FIELD_HOTFIX_CRITERIA
340
- : name;
354
+ : name === FIELD_DELIVERY_BRANCH_CLI_ALIAS
355
+ ? FIELD_DELIVERY_BRANCH
356
+ : name;
341
357
  for (const field of inspectAllPolicies(projectRoot)) {
342
358
  if (field.name === normalized)
343
359
  return field;
@@ -22,5 +22,7 @@ export type AtomicWriteBriefResult = {
22
22
  readonly message: string;
23
23
  };
24
24
  /** Validate then atomically persist a brief (temp file + rename). */
25
- export declare function atomicWriteBrief(filePath: string, data: JsonObject, vbriefRoot: string): AtomicWriteBriefResult;
25
+ export declare function atomicWriteBrief(filePath: string, data: JsonObject, vbriefRoot: string, options?: {
26
+ readonly projectRoot?: string;
27
+ }): AtomicWriteBriefResult;
26
28
  //# sourceMappingURL=brief-io.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { dirname, resolve } from "node:path";
3
3
  import { atomicWriteText } from "../cache/io.js";
4
4
  import { hasArtifactSuffix } from "../layout/resolve.js";
5
5
  import { pythonJsonPretty } from "../vbrief-build/json.js";
@@ -60,13 +60,15 @@ export function validateBriefForPersist(filePath, data, vbriefRoot) {
60
60
  return errors.join("; ");
61
61
  }
62
62
  /** Validate then atomically persist a brief (temp file + rename). */
63
- export function atomicWriteBrief(filePath, data, vbriefRoot) {
63
+ export function atomicWriteBrief(filePath, data, vbriefRoot, options = {}) {
64
64
  const validationError = validateBriefForPersist(filePath, data, vbriefRoot);
65
65
  if (validationError !== null) {
66
66
  return { ok: false, message: validationError };
67
67
  }
68
+ // #3042: contain against projectRoot (parent of xbrief/), not dirname(filePath).
69
+ const projectRoot = options.projectRoot ?? dirname(resolve(vbriefRoot));
68
70
  try {
69
- atomicWriteText(filePath, formatBriefJson(data));
71
+ atomicWriteText(filePath, formatBriefJson(data), { projectRoot });
70
72
  return { ok: true };
71
73
  }
72
74
  catch (err) {
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Delivery integrity for scope completion (#3041).
3
+ *
4
+ * A code-bearing scope must not acquire a delivered disposition unless the
5
+ * merge/delivered commit is an ancestor of the refreshed remote delivery ref,
6
+ * or an explicit auditable non-delivery disposition is recorded.
7
+ *
8
+ * Deploy / UAT are separate axes and MUST NOT be inferred from Git alone.
9
+ */
10
+ import { type GitRunner } from "../session/git.js";
11
+ /** Handoff states that Git delivery can assert (deploy/UAT never inferred). */
12
+ export declare const HANDOFF_STATES: readonly ["implemented", "pr_open", "merged_to_integration", "delivered"];
13
+ export type HandoffState = (typeof HANDOFF_STATES)[number];
14
+ /** Explicit non-delivery terminal dispositions (never render as shipped). */
15
+ export declare const NON_DELIVERY_DISPOSITIONS: readonly ["cancelled", "superseded", "experiment_archived", "accepted_not_delivered"];
16
+ export type NonDeliveryDisposition = (typeof NON_DELIVERY_DISPOSITIONS)[number];
17
+ /** Delivery verification outcome for completion provenance. */
18
+ export type DeliveryDisposition = "delivered" | "merged_to_integration" | "not_delivered" | NonDeliveryDisposition | "unknown" | "unverified";
19
+ export interface CompletionProvenance {
20
+ readonly repository: string | null;
21
+ readonly implementationCommit: string | null;
22
+ readonly prNumber: number | null;
23
+ readonly prBase: string | null;
24
+ readonly mergeCommit: string | null;
25
+ readonly deliveryBranch: string;
26
+ readonly deliveryCommit: string | null;
27
+ readonly verifiedAt: string;
28
+ readonly verifier: string;
29
+ readonly disposition: DeliveryDisposition;
30
+ readonly handoffState: HandoffState | "unknown";
31
+ /** Always null unless explicitly supplied — never inferred from Git (#3041). */
32
+ readonly deployed: boolean | null;
33
+ /** Always null unless explicitly supplied — never inferred from Git (#3041). */
34
+ readonly uatVerified: boolean | null;
35
+ }
36
+ export interface DeliveryEvidenceInput {
37
+ readonly repository?: string | null;
38
+ readonly implementationCommit?: string | null;
39
+ readonly prNumber?: number | null;
40
+ readonly prBase?: string | null;
41
+ readonly mergeCommit?: string | null;
42
+ readonly deliveryBranch?: string | null;
43
+ readonly deliveryCommit?: string | null;
44
+ readonly mergedAt?: string | null;
45
+ /** Explicit deploy/UAT (optional; never auto-filled from Git). */
46
+ readonly deployed?: boolean | null;
47
+ readonly uatVerified?: boolean | null;
48
+ readonly verifier?: string | null;
49
+ }
50
+ export interface DeliveryGateOptions {
51
+ readonly projectRoot: string;
52
+ readonly plan: Record<string, unknown>;
53
+ readonly nowIso: string;
54
+ readonly evidence?: DeliveryEvidenceInput | null;
55
+ readonly nonDeliveryDisposition?: NonDeliveryDisposition | null;
56
+ readonly runGit?: GitRunner;
57
+ readonly verifier?: string;
58
+ /**
59
+ * When true, skip remote refresh + ancestry (tests only that inject pre-validated
60
+ * evidence). Production callers leave this false.
61
+ */
62
+ readonly assumeEvidenceValidated?: boolean;
63
+ }
64
+ export interface DeliveryGateResult {
65
+ readonly ok: boolean;
66
+ readonly message: string;
67
+ readonly provenance: CompletionProvenance | null;
68
+ readonly codeBearing: boolean;
69
+ }
70
+ export declare function isNonDeliveryDisposition(value: unknown): value is NonDeliveryDisposition;
71
+ /** True when the brief is treated as code-bearing for the delivery gate (#3041). */
72
+ export declare function isCodeBearingScope(plan: Record<string, unknown>): boolean;
73
+ export declare function hasGithubIssueRef(plan: Record<string, unknown>): boolean;
74
+ /**
75
+ * Classify stored completion provenance for read paths.
76
+ * Legacy completed records without provenance → unknown/unverified (not delivered).
77
+ */
78
+ export declare function classifyStoredDeliveryDisposition(plan: Record<string, unknown>): DeliveryDisposition;
79
+ /**
80
+ * Refresh the remote delivery ref. Failure blocks delivered completion (#3041).
81
+ */
82
+ export declare function refreshRemoteDeliveryRef(projectRoot: string, deliveryBranch: string, runGit?: GitRunner): {
83
+ ok: boolean;
84
+ error: string | null;
85
+ remoteRef: string;
86
+ };
87
+ /**
88
+ * Validate that mergeCommit is an ancestor of the refreshed remote delivery ref.
89
+ */
90
+ export declare function verifyDeliveryAncestry(projectRoot: string, mergeCommit: string, deliveryBranch: string, runGit?: GitRunner): {
91
+ ok: boolean;
92
+ error: string | null;
93
+ remoteTip: string | null;
94
+ };
95
+ /**
96
+ * Gate delivered completion for a code-bearing scope (#3041).
97
+ *
98
+ * Returns ok=true with provenance when:
99
+ * - scope is not code-bearing (provenance may be null or non-code note), OR
100
+ * - explicit non-delivery disposition is provided, OR
101
+ * - durable delivery evidence validates (PR base == deliveryBranch, merge commit
102
+ * ancestor of refreshed remote delivery ref).
103
+ */
104
+ export declare function evaluateDeliveryGate(options: DeliveryGateOptions): DeliveryGateResult;
105
+ /** Stamp completion provenance onto plan.metadata (mutates plan). */
106
+ export declare function stampDeliveryProvenance(plan: Record<string, unknown>, provenance: CompletionProvenance): void;
107
+ /**
108
+ * Parse a PR REST payload into delivery evidence fields.
109
+ * Expects GitHub pulls API shape (merged_at, base.ref, merge_commit_sha, …).
110
+ */
111
+ export declare function evidenceFromPrPayload(payload: Record<string, unknown>, prNumber: number, repository: string | null, deliveryBranch?: string | null): DeliveryEvidenceInput;
112
+ //# sourceMappingURL=delivery-evidence.d.ts.map