@deftai/directive-core 0.87.0 → 0.89.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/actions.d.ts +55 -0
- package/dist/authz/actions.js +125 -0
- package/dist/authz/classify.d.ts +23 -0
- package/dist/authz/classify.js +217 -0
- package/dist/authz/closed-verb.d.ts +42 -0
- package/dist/authz/closed-verb.js +279 -0
- package/dist/authz/evaluate.d.ts +41 -0
- package/dist/authz/evaluate.js +298 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.js +15 -0
- package/dist/authz/origin.d.ts +28 -0
- package/dist/authz/origin.js +64 -0
- package/dist/authz/paths.d.ts +6 -0
- package/dist/authz/paths.js +19 -0
- package/dist/authz/store.d.ts +40 -0
- package/dist/authz/store.js +317 -0
- package/dist/authz/templates.d.ts +139 -0
- package/dist/authz/templates.js +241 -0
- package/dist/authz/types.d.ts +111 -0
- package/dist/authz/types.js +41 -0
- package/dist/authz/verb-classification.d.ts +44 -0
- package/dist/authz/verb-classification.js +237 -0
- package/dist/branch/evaluate.js +6 -1
- package/dist/cache/fetch.js +10 -5
- package/dist/cache/io.d.ts +9 -2
- package/dist/cache/io.js +30 -10
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/cache/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/check/gate-lists.js +1 -0
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +208 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/escalation/actions.d.ts +63 -0
- package/dist/escalation/actions.js +137 -0
- package/dist/escalation/index.d.ts +8 -0
- package/dist/escalation/index.js +8 -0
- package/dist/escalation/paths.d.ts +3 -0
- package/dist/escalation/paths.js +10 -0
- package/dist/escalation/store.d.ts +17 -0
- package/dist/escalation/store.js +172 -0
- package/dist/escalation/types.d.ts +73 -0
- package/dist/escalation/types.js +43 -0
- package/dist/eval/crud-telemetry.js +30 -10
- package/dist/eval/health.js +22 -7
- package/dist/eval/readback.js +11 -10
- package/dist/eval/run.js +32 -16
- package/dist/events/attribution-enrichment.js +10 -4
- package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
- package/dist/finish-loop/directive-finish-loop.js +239 -0
- package/dist/finish-loop/grant-gate.d.ts +31 -0
- package/dist/finish-loop/grant-gate.js +169 -0
- package/dist/finish-loop/index.d.ts +11 -0
- package/dist/finish-loop/index.js +11 -0
- package/dist/finish-loop/main.d.ts +35 -0
- package/dist/finish-loop/main.js +365 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
- package/dist/finish-loop/pr-finish-loop.js +217 -0
- package/dist/finish-loop/progress.d.ts +17 -0
- package/dist/finish-loop/progress.js +45 -0
- package/dist/finish-loop/queue.d.ts +16 -0
- package/dist/finish-loop/queue.js +65 -0
- package/dist/finish-loop/types.d.ts +52 -0
- package/dist/finish-loop/types.js +10 -0
- package/dist/fs/contained-write.d.ts +101 -0
- package/dist/fs/contained-write.js +281 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/classify/classify.d.ts +10 -0
- package/dist/hooks/classify/classify.js +37 -0
- package/dist/hooks/classify/index.d.ts +14 -0
- package/dist/hooks/classify/index.js +13 -0
- package/dist/hooks/classify/paths.d.ts +22 -0
- package/dist/hooks/classify/paths.js +72 -0
- package/dist/hooks/classify/payload.d.ts +16 -0
- package/dist/hooks/classify/payload.js +45 -0
- package/dist/hooks/classify/stdin.d.ts +12 -0
- package/dist/hooks/classify/stdin.js +63 -0
- package/dist/hooks/classify/tool-name.d.ts +18 -0
- package/dist/hooks/classify/tool-name.js +85 -0
- package/dist/hooks/classify/types.d.ts +41 -0
- package/dist/hooks/classify/types.js +7 -0
- package/dist/hooks/dispatcher.d.ts +30 -15
- package/dist/hooks/dispatcher.js +326 -134
- package/dist/hooks/fixtures/cases.d.ts +44 -0
- package/dist/hooks/fixtures/cases.js +595 -0
- package/dist/hooks/fixtures/index.d.ts +2 -0
- package/dist/hooks/fixtures/index.js +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +65 -10
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +12 -5
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +42 -29
- package/dist/init-deposit/xbrief-projections.js +17 -10
- package/dist/intake/candidates-log.js +46 -35
- package/dist/intake/github-body-cli.d.ts +6 -0
- package/dist/intake/github-body-cli.js +17 -0
- package/dist/intake/github-body.d.ts +46 -3
- package/dist/intake/github-body.js +191 -20
- package/dist/intake/issue-emit.d.ts +1 -0
- package/dist/intake/issue-emit.js +41 -23
- package/dist/intake/issue-ingest.js +11 -2
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +29 -20
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +172 -13
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/plan-sequence/store.js +11 -4
- package/dist/policy/hotfix-criteria.d.ts +79 -0
- package/dist/policy/hotfix-criteria.js +197 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.js +32 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +110 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +269 -27
- package/dist/policy/require-human-merge.d.ts +75 -0
- package/dist/policy/require-human-merge.js +324 -0
- package/dist/policy/resolve.js +17 -6
- package/dist/policy/runtime-authority.d.ts +56 -2
- package/dist/policy/runtime-authority.js +297 -3
- package/dist/policy/write-fence.d.ts +78 -0
- package/dist/policy/write-fence.js +164 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -0
- package/dist/preflight/evaluate.js +10 -0
- package/dist/product-signal/consent.js +21 -5
- package/dist/product-signal/submit.js +22 -12
- package/dist/release/build-dist-runner.js +5 -2
- package/dist/release/build-dist.d.ts +19 -1
- package/dist/release/build-dist.js +50 -2
- package/dist/release/native-steps.js +7 -2
- package/dist/release-publish/pipeline.d.ts +13 -0
- package/dist/release-publish/pipeline.js +46 -1
- package/dist/scope/audit-log.js +19 -24
- package/dist/scope/decompose.js +10 -5
- package/dist/scope/decomposed-refs.js +18 -3
- package/dist/scope/demote.js +9 -2
- package/dist/scope/undo.js +9 -2
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.js +19 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +37 -0
- package/dist/staleness-tickler/state.js +26 -6
- package/dist/swarm/launch.js +13 -3
- package/dist/swarm/routing.js +9 -3
- package/dist/task-surface/index.js +24 -9
- package/dist/triage/bootstrap/gitignore.js +53 -10
- package/dist/triage/cache-path.js +10 -3
- package/dist/triage/welcome/summary.js +11 -4
- package/dist/triage/welcome/writers.js +45 -16
- package/dist/validate-content/validate-links.js +5 -0
- package/dist/value/readback.js +11 -6
- package/dist/vbrief-activate/activate.js +12 -4
- package/dist/vbrief-build/project-definition-io.js +14 -6
- package/dist/verify-source/contained-writes.d.ts +59 -0
- package/dist/verify-source/contained-writes.js +272 -0
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +19 -1
- package/dist/verify-source/verify-stubs.js +3 -0
- package/dist/xbrief-migrate/migrate-project.js +26 -12
- package/dist/xbrief-migrate/transforms.d.ts +4 -2
- package/dist/xbrief-migrate/transforms.js +37 -14
- package/package.json +16 -3
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human merge gate (#1193 R3 / Wave 2 of #2948).
|
|
3
|
+
*
|
|
4
|
+
* When requireHumanMerge is true, agents may open PRs but must not merge.
|
|
5
|
+
* Three enforcement surfaces (mirror #747):
|
|
6
|
+
* (1) merge preflight refuses agent-initiated merge
|
|
7
|
+
* (2) verify:branch aggregate surfaces the policy
|
|
8
|
+
* (3) branch-protection / setup docs require ≥1 human reviewer
|
|
9
|
+
*
|
|
10
|
+
* Override: `task policy:allow-bot-merge -- --confirm` + `DEFT_ALLOW_BOT_MERGE=1`.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
14
|
+
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
15
|
+
import { policyColonInvocation } from "./policy-invocation.js";
|
|
16
|
+
import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
|
|
17
|
+
export const FIELD_REQUIRE_HUMAN_MERGE = "plan.policy.requireHumanMerge";
|
|
18
|
+
export const FIELD_REQUIRE_HUMAN_MERGE_CLI_ALIAS = "requireHumanMerge";
|
|
19
|
+
export const FIELD_AUTO_DEPLOY_ON_MERGE = "plan.policy.autoDeployOnMerge";
|
|
20
|
+
/** Emergency env-var bypass for agent merge (#1193). */
|
|
21
|
+
export const ENV_ALLOW_BOT_MERGE = "DEFT_ALLOW_BOT_MERGE";
|
|
22
|
+
/** Legacy narrative key recognized at read time with deprecation warning. */
|
|
23
|
+
export const LEGACY_REQUIRE_HUMAN_MERGE_KEY = "Require human merge";
|
|
24
|
+
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
25
|
+
function envBypassActive(env = process.env) {
|
|
26
|
+
const raw = env[ENV_ALLOW_BOT_MERGE] ?? "";
|
|
27
|
+
return TRUTHY.has(raw.trim().toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
function coerceLegacyNarrative(value) {
|
|
30
|
+
if (typeof value === "boolean")
|
|
31
|
+
return value;
|
|
32
|
+
if (typeof value !== "string")
|
|
33
|
+
return false;
|
|
34
|
+
const low = value.trim().toLowerCase();
|
|
35
|
+
if (["true", "yes", "on", "1"].includes(low))
|
|
36
|
+
return true;
|
|
37
|
+
if (/:\s*(true|yes|on|1)\b/.test(low))
|
|
38
|
+
return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function readAutoDeploy(policyBlock) {
|
|
42
|
+
const raw = policyBlock.autoDeployOnMerge;
|
|
43
|
+
return typeof raw === "boolean" ? raw : false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve effective human-merge policy (#1193).
|
|
47
|
+
*
|
|
48
|
+
* Order:
|
|
49
|
+
* 1. DEFT_ALLOW_BOT_MERGE=1 → requireHumanMerge effective false (env-bypass)
|
|
50
|
+
* 2. typed plan.policy.requireHumanMerge
|
|
51
|
+
* 3. legacy narrative "Require human merge"
|
|
52
|
+
* 4. autoDeployOnMerge true → default requireHumanMerge true
|
|
53
|
+
* 5. default false (no auto-deploy → agents may merge when other gates allow)
|
|
54
|
+
*/
|
|
55
|
+
export function resolveHumanMergePolicy(projectRoot, env = process.env) {
|
|
56
|
+
if (envBypassActive(env)) {
|
|
57
|
+
return {
|
|
58
|
+
requireHumanMerge: false,
|
|
59
|
+
source: "env-bypass",
|
|
60
|
+
deprecationWarning: null,
|
|
61
|
+
error: null,
|
|
62
|
+
autoDeployOnMerge: false,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const [data, err] = loadProjectDefinition(projectRoot);
|
|
66
|
+
if (data === null) {
|
|
67
|
+
return {
|
|
68
|
+
requireHumanMerge: false,
|
|
69
|
+
source: "default",
|
|
70
|
+
deprecationWarning: null,
|
|
71
|
+
error: err,
|
|
72
|
+
autoDeployOnMerge: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const plan = data.plan;
|
|
76
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
77
|
+
return {
|
|
78
|
+
requireHumanMerge: false,
|
|
79
|
+
source: "default",
|
|
80
|
+
deprecationWarning: null,
|
|
81
|
+
error: "PROJECT-DEFINITION 'plan' is not an object",
|
|
82
|
+
autoDeployOnMerge: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const planObj = plan;
|
|
86
|
+
const policyRaw = readPlanPolicy(planObj);
|
|
87
|
+
const policyBlock = typeof policyRaw === "object" && policyRaw !== null && !Array.isArray(policyRaw)
|
|
88
|
+
? policyRaw
|
|
89
|
+
: {};
|
|
90
|
+
const autoDeployOnMerge = readAutoDeploy(policyBlock);
|
|
91
|
+
if ("requireHumanMerge" in policyBlock) {
|
|
92
|
+
const raw = policyBlock.requireHumanMerge;
|
|
93
|
+
if (typeof raw !== "boolean") {
|
|
94
|
+
return {
|
|
95
|
+
requireHumanMerge: true, // fail closed on malformed flag when present
|
|
96
|
+
source: "typed",
|
|
97
|
+
deprecationWarning: null,
|
|
98
|
+
error: `plan.policy.requireHumanMerge must be a boolean; got ${typeof raw}`,
|
|
99
|
+
autoDeployOnMerge,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
requireHumanMerge: raw,
|
|
104
|
+
source: "typed",
|
|
105
|
+
deprecationWarning: null,
|
|
106
|
+
error: null,
|
|
107
|
+
autoDeployOnMerge,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const narratives = planObj.narratives;
|
|
111
|
+
if (typeof narratives === "object" &&
|
|
112
|
+
narratives !== null &&
|
|
113
|
+
!Array.isArray(narratives) &&
|
|
114
|
+
LEGACY_REQUIRE_HUMAN_MERGE_KEY in narratives) {
|
|
115
|
+
const allow = coerceLegacyNarrative(narratives[LEGACY_REQUIRE_HUMAN_MERGE_KEY]);
|
|
116
|
+
const warn = `DEPRECATED: PROJECT-DEFINITION uses the legacy narrative key ` +
|
|
117
|
+
`'${LEGACY_REQUIRE_HUMAN_MERGE_KEY}'. Migrate to typed ` +
|
|
118
|
+
`plan.policy.requireHumanMerge (#1193).`;
|
|
119
|
+
return {
|
|
120
|
+
requireHumanMerge: allow,
|
|
121
|
+
source: "legacy-narrative",
|
|
122
|
+
deprecationWarning: warn,
|
|
123
|
+
error: null,
|
|
124
|
+
autoDeployOnMerge,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (autoDeployOnMerge) {
|
|
128
|
+
return {
|
|
129
|
+
requireHumanMerge: true,
|
|
130
|
+
source: "auto-deploy-default",
|
|
131
|
+
deprecationWarning: null,
|
|
132
|
+
error: null,
|
|
133
|
+
autoDeployOnMerge: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
requireHumanMerge: false,
|
|
138
|
+
source: "default",
|
|
139
|
+
deprecationWarning: null,
|
|
140
|
+
error: null,
|
|
141
|
+
autoDeployOnMerge: false,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Session-start / AGENTS disclosure line when the human merge gate is ON. */
|
|
145
|
+
export function humanMergeDisclosureLine(result) {
|
|
146
|
+
if (result.source === "env-bypass") {
|
|
147
|
+
return (`[deft policy] ${ENV_ALLOW_BOT_MERGE} is set -- ` +
|
|
148
|
+
"human merge gate bypassed for this session (agent may merge).");
|
|
149
|
+
}
|
|
150
|
+
if (!result.requireHumanMerge) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return (`[deft policy] Human merge gate is ON for this project (source: ${result.source}); ` +
|
|
154
|
+
"agent may open PRs, may not merge.");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Merge preflight: refuse agent-initiated merge when requireHumanMerge is true.
|
|
158
|
+
* Surface (1) of the three-surface enforcement pattern.
|
|
159
|
+
*/
|
|
160
|
+
export function evaluateAgentMerge(projectRoot, env = process.env) {
|
|
161
|
+
const policy = resolveHumanMergePolicy(projectRoot, env);
|
|
162
|
+
if (!policy.requireHumanMerge) {
|
|
163
|
+
return {
|
|
164
|
+
exitCode: 0,
|
|
165
|
+
allowed: true,
|
|
166
|
+
message: policy.source === "env-bypass"
|
|
167
|
+
? `✓ human-merge gate: ${ENV_ALLOW_BOT_MERGE}=1 bypass -- agent merge allowed.`
|
|
168
|
+
: "✓ human-merge gate: requireHumanMerge is off -- agent merge allowed by policy.",
|
|
169
|
+
policy,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const parts = [
|
|
173
|
+
"❌ deft human-merge gate: refusing agent-initiated merge (#1193).",
|
|
174
|
+
"",
|
|
175
|
+
` Source: policy=${policy.source}`,
|
|
176
|
+
" plan.policy.requireHumanMerge is true — agents may open PRs, may not merge.",
|
|
177
|
+
];
|
|
178
|
+
if (policy.error !== null) {
|
|
179
|
+
parts.push(` Error: ${policy.error}`);
|
|
180
|
+
}
|
|
181
|
+
if (policy.deprecationWarning !== null) {
|
|
182
|
+
parts.push(` Note: ${policy.deprecationWarning}`);
|
|
183
|
+
}
|
|
184
|
+
parts.push("", " How to proceed:", " • Ask a human to review and merge the PR.", " • Or opt out via the typed surface (capability cost):", ` ${policyColonInvocation("allow-bot-merge", " -- --confirm")}`, ` • Or set the emergency-escape env-var: ${ENV_ALLOW_BOT_MERGE}=1`);
|
|
185
|
+
return {
|
|
186
|
+
exitCode: 1,
|
|
187
|
+
allowed: false,
|
|
188
|
+
message: parts.join("\n"),
|
|
189
|
+
policy,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* verify:branch aggregate surface (2): advisory line when gate is ON.
|
|
194
|
+
* Does not fail the branch gate on feature branches — merge refuse is surface (1).
|
|
195
|
+
*/
|
|
196
|
+
export function humanMergeBranchNote(result) {
|
|
197
|
+
if (result.source === "env-bypass") {
|
|
198
|
+
return humanMergeDisclosureLine(result);
|
|
199
|
+
}
|
|
200
|
+
if (!result.requireHumanMerge)
|
|
201
|
+
return null;
|
|
202
|
+
return (`ℹ deft human-merge: requireHumanMerge=true (source: ${result.source}) — ` +
|
|
203
|
+
"agent-initiated merge will be refused; human must merge.");
|
|
204
|
+
}
|
|
205
|
+
/** Inspector for policy:show --field=requireHumanMerge. */
|
|
206
|
+
export function inspectRequireHumanMerge(data, projectRoot) {
|
|
207
|
+
if (projectRoot !== undefined && projectRoot.length > 0) {
|
|
208
|
+
const resolved = resolveHumanMergePolicy(projectRoot);
|
|
209
|
+
return {
|
|
210
|
+
name: FIELD_REQUIRE_HUMAN_MERGE,
|
|
211
|
+
current: resolved.requireHumanMerge,
|
|
212
|
+
default: false,
|
|
213
|
+
source: resolved.source,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (data === null) {
|
|
217
|
+
return {
|
|
218
|
+
name: FIELD_REQUIRE_HUMAN_MERGE,
|
|
219
|
+
current: false,
|
|
220
|
+
default: false,
|
|
221
|
+
source: "default",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
225
|
+
if (typeof policyBlock === "object" &&
|
|
226
|
+
policyBlock !== null &&
|
|
227
|
+
!Array.isArray(policyBlock) &&
|
|
228
|
+
"requireHumanMerge" in policyBlock) {
|
|
229
|
+
const raw = policyBlock.requireHumanMerge;
|
|
230
|
+
return {
|
|
231
|
+
name: FIELD_REQUIRE_HUMAN_MERGE,
|
|
232
|
+
current: typeof raw === "boolean" ? raw : true,
|
|
233
|
+
default: false,
|
|
234
|
+
source: "typed",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (typeof policyBlock === "object" &&
|
|
238
|
+
policyBlock !== null &&
|
|
239
|
+
!Array.isArray(policyBlock) &&
|
|
240
|
+
policyBlock.autoDeployOnMerge === true) {
|
|
241
|
+
return {
|
|
242
|
+
name: FIELD_REQUIRE_HUMAN_MERGE,
|
|
243
|
+
current: true,
|
|
244
|
+
default: false,
|
|
245
|
+
source: "auto-deploy-default",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
name: FIELD_REQUIRE_HUMAN_MERGE,
|
|
250
|
+
current: false,
|
|
251
|
+
default: false,
|
|
252
|
+
source: "default",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export const ALLOW_BOT_MERGE_CAPABILITY_COST = "\u26a0 Capability-cost disclosure -- allowing bot/agent merge turns OFF the " +
|
|
256
|
+
"deft human merge gate (#1193).\n" +
|
|
257
|
+
" \u2022 Agents may call `gh pr merge` / `task pr:wait-mergeable-and-merge`.\n" +
|
|
258
|
+
" \u2022 If autoDeployOnMerge is true, agent merges can reach production unattended.\n" +
|
|
259
|
+
" \u2022 Reversible: set plan.policy.requireHumanMerge=true or re-run setup.\n" +
|
|
260
|
+
" \u2022 Change is recorded to meta/policy-changes.log for auditability.";
|
|
261
|
+
/** Write requireHumanMerge=false (allow bot merge) with audit trail. */
|
|
262
|
+
export function setRequireHumanMerge(projectRoot, options) {
|
|
263
|
+
const { requireHumanMerge, actor = "agent", note = "" } = options;
|
|
264
|
+
const path = projectDefinitionPath(projectRoot);
|
|
265
|
+
if (!existsSync(path)) {
|
|
266
|
+
throw new Error(`PROJECT-DEFINITION not found at ${path}`);
|
|
267
|
+
}
|
|
268
|
+
return projectDefinitionMutationLock(projectRoot, () => {
|
|
269
|
+
const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
|
|
270
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
271
|
+
throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
|
|
272
|
+
}
|
|
273
|
+
const data = parsed;
|
|
274
|
+
if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
|
|
275
|
+
if (data.plan === undefined) {
|
|
276
|
+
data.plan = {};
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
throw new Error("PROJECT-DEFINITION 'plan' is not an object");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const plan = data.plan;
|
|
283
|
+
migrateLegacyPolicyKey(plan);
|
|
284
|
+
const existingPolicy = plan[PLAN_POLICY_KEY];
|
|
285
|
+
if (typeof existingPolicy !== "object" ||
|
|
286
|
+
existingPolicy === null ||
|
|
287
|
+
Array.isArray(existingPolicy)) {
|
|
288
|
+
if (existingPolicy === undefined) {
|
|
289
|
+
plan[PLAN_POLICY_KEY] = {};
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
throw new Error("plan.policy is not an object");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const policyBlock = plan[PLAN_POLICY_KEY];
|
|
296
|
+
const previous = policyBlock.requireHumanMerge;
|
|
297
|
+
policyBlock.requireHumanMerge = Boolean(requireHumanMerge);
|
|
298
|
+
let legacyDropped = false;
|
|
299
|
+
const narratives = plan.narratives;
|
|
300
|
+
if (typeof narratives === "object" &&
|
|
301
|
+
narratives !== null &&
|
|
302
|
+
!Array.isArray(narratives) &&
|
|
303
|
+
LEGACY_REQUIRE_HUMAN_MERGE_KEY in narratives) {
|
|
304
|
+
delete narratives[LEGACY_REQUIRE_HUMAN_MERGE_KEY];
|
|
305
|
+
legacyDropped = true;
|
|
306
|
+
}
|
|
307
|
+
atomicWriteProjectDefinition(path, data);
|
|
308
|
+
const changed = previous !== Boolean(requireHumanMerge) || legacyDropped;
|
|
309
|
+
const parts = [
|
|
310
|
+
`actor=${actor}`,
|
|
311
|
+
`requireHumanMerge=${requireHumanMerge ? "true" : "false"}`,
|
|
312
|
+
`previous=${previous === undefined ? "None" : String(previous)}`,
|
|
313
|
+
];
|
|
314
|
+
if (legacyDropped)
|
|
315
|
+
parts.push("legacy-narrative-migrated=true");
|
|
316
|
+
if (note) {
|
|
317
|
+
parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
|
|
318
|
+
}
|
|
319
|
+
const auditEntry = parts.join(" ");
|
|
320
|
+
appendAuditLog(projectRoot, auditEntry);
|
|
321
|
+
return { changed, auditEntry };
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=require-human-merge.js.map
|
package/dist/policy/resolve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve as pathResolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
4
5
|
import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
5
6
|
import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
|
|
@@ -177,15 +178,25 @@ function nowIso() {
|
|
|
177
178
|
}
|
|
178
179
|
/** Append a one-line audit entry to meta/policy-changes.log (#746). */
|
|
179
180
|
export function appendAuditLog(projectRoot, entry) {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const root = pathResolve(projectRoot);
|
|
182
|
+
const logPath = join(root, AUDIT_LOG_REL_PATH);
|
|
183
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
183
184
|
if (!existsSync(logPath)) {
|
|
184
185
|
const header = "# meta/policy-changes.log -- audit trail for " +
|
|
185
186
|
"policy.allowDirectCommitsToMaster transitions (#746)\n";
|
|
186
|
-
|
|
187
|
+
containedWrite({
|
|
188
|
+
root,
|
|
189
|
+
target: AUDIT_LOG_REL_PATH,
|
|
190
|
+
data: header,
|
|
191
|
+
mode: "create",
|
|
192
|
+
});
|
|
187
193
|
}
|
|
188
|
-
|
|
194
|
+
containedWrite({
|
|
195
|
+
root,
|
|
196
|
+
target: AUDIT_LOG_REL_PATH,
|
|
197
|
+
data: `${nowIso()} ${entry}\n`,
|
|
198
|
+
mode: "append",
|
|
199
|
+
});
|
|
189
200
|
return logPath;
|
|
190
201
|
}
|
|
191
202
|
/** Write the typed policy flag back to PROJECT-DEFINITION (#746). */
|
|
@@ -9,6 +9,8 @@ export interface RuntimeAuthorityScopes {
|
|
|
9
9
|
/** merge / close / release — enforced only when Shell payload is classifiable (host gap). */
|
|
10
10
|
readonly merge: boolean;
|
|
11
11
|
}
|
|
12
|
+
/** Layers that contribute to a resolved write fence (#516 / #2443 / #2948 Wave 3). */
|
|
13
|
+
export type WriteFenceSource = "project" | "story";
|
|
12
14
|
export interface RuntimeAuthorityPolicy {
|
|
13
15
|
readonly enabled: boolean;
|
|
14
16
|
/** When non-empty, write targets must match at least one pattern. Empty = allow all paths. */
|
|
@@ -16,6 +18,17 @@ export interface RuntimeAuthorityPolicy {
|
|
|
16
18
|
/** Deny wins over allow. */
|
|
17
19
|
readonly denyPaths: readonly string[];
|
|
18
20
|
readonly scopes: RuntimeAuthorityScopes;
|
|
21
|
+
/**
|
|
22
|
+
* Optional story-layer allow globs from `plan.metadata.swarm.file_scope`
|
|
23
|
+
* (populated by `resolveWriteFence` only). When non-empty, path must match
|
|
24
|
+
* project allowPaths (if any) **and** this layer. Empty/undefined = no story narrowing.
|
|
25
|
+
*/
|
|
26
|
+
readonly storyAllowPaths?: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Which layers contributed to this policy (for stable deny reason codes).
|
|
29
|
+
* Set by `resolveWriteFence`; absent for bare project policy from disk.
|
|
30
|
+
*/
|
|
31
|
+
readonly fenceSources?: readonly WriteFenceSource[];
|
|
19
32
|
}
|
|
20
33
|
export declare const DEFAULT_RUNTIME_AUTHORITY_SCOPES: RuntimeAuthorityScopes;
|
|
21
34
|
/** Safe default: opt-in only — existing consumers see no runtime authority until enabled. */
|
|
@@ -34,8 +47,8 @@ export declare function inspectRuntimeAuthority(data: Record<string, unknown> |
|
|
|
34
47
|
export declare function loadRuntimeAuthorityPolicy(data: Record<string, unknown> | null): RuntimeAuthorityPolicy;
|
|
35
48
|
/** Load runtime authority from a project root (PROJECT-DEFINITION on disk). */
|
|
36
49
|
export declare function loadRuntimeAuthorityFromProject(projectRoot: string): RuntimeAuthorityPolicy;
|
|
37
|
-
export type RuntimeAuthorityPathVerdict = "allow" | "deny-allowlist" | "deny-denylist";
|
|
38
|
-
/** Evaluate a project-relative POSIX path against allow/deny globs. */
|
|
50
|
+
export type RuntimeAuthorityPathVerdict = "allow" | "deny-allowlist" | "deny-denylist" | "deny-story-scope";
|
|
51
|
+
/** Evaluate a project-relative POSIX path against allow/deny globs (+ optional story layer). */
|
|
39
52
|
export declare function evaluateRuntimeAuthorityPath(policy: RuntimeAuthorityPolicy, relPathPosix: string): RuntimeAuthorityPathVerdict;
|
|
40
53
|
export interface RuntimeAuthorityDirectWriteInput {
|
|
41
54
|
readonly policy: RuntimeAuthorityPolicy;
|
|
@@ -51,4 +64,45 @@ export interface RuntimeAuthorityDirectWriteResult {
|
|
|
51
64
|
* Unclassifiable paths (null) pass — host gap documented for Shell/MCP.
|
|
52
65
|
*/
|
|
53
66
|
export declare function evaluateRuntimeAuthorityDirectWrite(input: RuntimeAuthorityDirectWriteInput): RuntimeAuthorityDirectWriteResult;
|
|
67
|
+
/** Classifiable shell/MCP operation for scopes.push / scopes.merge (#2711). */
|
|
68
|
+
export type RuntimeAuthorityShellOp = "push" | "merge";
|
|
69
|
+
/**
|
|
70
|
+
* List all classifiable push/merge ops in a shell command (#2711).
|
|
71
|
+
* Scans every list/pipeline/newline segment so compound commands like
|
|
72
|
+
* `gh pr merge 1 && git push` surface both ops (dispatcher evaluates each).
|
|
73
|
+
* Newlines are delimiters so multi-line scripts cannot hide a later push/merge.
|
|
74
|
+
*/
|
|
75
|
+
export declare function listShellOps(command: string): RuntimeAuthorityShellOp[];
|
|
76
|
+
/**
|
|
77
|
+
* Classify a shell command string for push/merge scopes (#2711).
|
|
78
|
+
* Unclassifiable commands return null (fail open at the gate).
|
|
79
|
+
* When a compound command has multiple ops, returns the first of listShellOps
|
|
80
|
+
* (push before merge); prefer listShellOps + evaluate-each for enforcement.
|
|
81
|
+
*
|
|
82
|
+
* Patterns (intentionally narrow; prefer false-open over false-deny):
|
|
83
|
+
* - push: `git push`, `git.exe push`, with optional env / -C / -c prefixes
|
|
84
|
+
* - merge: `gh pr merge`, `gh.exe pr merge`
|
|
85
|
+
*/
|
|
86
|
+
export declare function classifyShellCommand(command: string): RuntimeAuthorityShellOp | null;
|
|
87
|
+
/**
|
|
88
|
+
* Classify an MCP (or MCP-like) tool name + optional argument blob for push/merge (#2711).
|
|
89
|
+
* Returns null when the tool is not a known push/merge mutation (fail open).
|
|
90
|
+
*/
|
|
91
|
+
export declare function classifyMcpTool(toolName: string, argsText?: string | null): RuntimeAuthorityShellOp | null;
|
|
92
|
+
export interface RuntimeAuthorityShellOpInput {
|
|
93
|
+
readonly policy: RuntimeAuthorityPolicy;
|
|
94
|
+
readonly op: RuntimeAuthorityShellOp | null;
|
|
95
|
+
}
|
|
96
|
+
export interface RuntimeAuthorityShellOpResult {
|
|
97
|
+
readonly allowed: boolean;
|
|
98
|
+
readonly reason: string | null;
|
|
99
|
+
readonly code: "runtime-policy-deny-scope" | null;
|
|
100
|
+
/** True when op was null — gate should fail open. */
|
|
101
|
+
readonly unclassifiable: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate scopes.push / scopes.merge for a classifiable shell/MCP operation (#2711).
|
|
105
|
+
* null op → allow (unclassifiable fail-open). disabled policy → allow.
|
|
106
|
+
*/
|
|
107
|
+
export declare function evaluateRuntimeAuthorityShellOp(input: RuntimeAuthorityShellOpInput): RuntimeAuthorityShellOpResult;
|
|
54
108
|
//# sourceMappingURL=runtime-authority.d.ts.map
|