@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
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
/** Port of scripts/pack_render.py — content-pack projection renderer (#1294, #1295). */
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { contentRoot } from "../content-root.js";
|
|
6
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
7
|
+
/** Write under the parent directory as containment root (#2980 wave D). */
|
|
8
|
+
function writeContainedFile(output, text) {
|
|
9
|
+
const abs = resolve(output);
|
|
10
|
+
const dir = dirname(abs);
|
|
11
|
+
mkdirSync(dir, { recursive: true });
|
|
12
|
+
containedWrite({
|
|
13
|
+
root: dir,
|
|
14
|
+
target: basename(abs),
|
|
15
|
+
data: text,
|
|
16
|
+
mode: "replace",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
6
19
|
export function resolveRepoRoot() {
|
|
7
20
|
return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
8
21
|
}
|
|
@@ -54,6 +67,138 @@ const SWARM_SPEC_BANNER = [
|
|
|
54
67
|
];
|
|
55
68
|
export const BANNER = `${LESSONS_BANNER.join("\n")}\n`;
|
|
56
69
|
export const DOC_TITLE = "# Lessons Learned";
|
|
70
|
+
/** Non-zero exit code for a pack-render containment / schema refusal. */
|
|
71
|
+
export const PACK_RENDER_REFUSED_EXIT_CODE = 2;
|
|
72
|
+
/**
|
|
73
|
+
* Thrown, fail-closed, when a pack record's projection path escapes CONTENT_ROOT,
|
|
74
|
+
* violates the pack path schema, or a frontmatter field carries an injection
|
|
75
|
+
* payload (null byte / newline). Remediates AppSec finding skill-pack-supply-01
|
|
76
|
+
* from tracker #2904. Refs #2914.
|
|
77
|
+
*/
|
|
78
|
+
export class PackRenderContainmentError extends Error {
|
|
79
|
+
pack;
|
|
80
|
+
field;
|
|
81
|
+
offendingValue;
|
|
82
|
+
constructor(message, details) {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "PackRenderContainmentError";
|
|
85
|
+
this.pack = details.pack;
|
|
86
|
+
this.field = details.field;
|
|
87
|
+
this.offendingValue = details.offendingValue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Path-SEGMENT containment: is `child` equal to `parent` or nested under it?
|
|
92
|
+
* Uses `path.relative` so `/foo` is NOT treated as containing `/foobar`
|
|
93
|
+
* (mirrors deposit/contain.ts).
|
|
94
|
+
*/
|
|
95
|
+
function isContained(parent, child) {
|
|
96
|
+
if (parent === child) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const rel = relative(parent, child);
|
|
100
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
101
|
+
}
|
|
102
|
+
/** realpath with a resolve() fallback when the path does not exist yet. */
|
|
103
|
+
function safeRealpath(p) {
|
|
104
|
+
try {
|
|
105
|
+
return realpathSync(p);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return resolve(p);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Defense-in-depth: walk each EXISTING component from CONTENT_ROOT down to the
|
|
113
|
+
* resolved output and refuse any symlink that escapes CONTENT_ROOT. Mirrors the
|
|
114
|
+
* realpath-anchored segment walk in deposit/contain.ts so a symlinked content
|
|
115
|
+
* subtree cannot redirect a projection write outside the shippable-content root.
|
|
116
|
+
*/
|
|
117
|
+
function assertAncestorContained(packName, field, rawPath, rootReal, outPath) {
|
|
118
|
+
const rel = relative(rootReal, outPath);
|
|
119
|
+
const segments = rel.split(/[\\/]+/).filter((segment) => segment.length > 0);
|
|
120
|
+
let current = rootReal;
|
|
121
|
+
for (const segment of segments) {
|
|
122
|
+
current = join(current, segment);
|
|
123
|
+
let info;
|
|
124
|
+
try {
|
|
125
|
+
info = lstatSync(current);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Component does not exist yet; nothing below it can exist either.
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (info.isSymbolicLink()) {
|
|
132
|
+
let linkReal;
|
|
133
|
+
try {
|
|
134
|
+
linkReal = realpathSync(current);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} path component ${current} is a broken symlink`, { pack: packName, field, offendingValue: rawPath });
|
|
138
|
+
}
|
|
139
|
+
if (!isContained(rootReal, linkReal)) {
|
|
140
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} path component ${current} is a symlink ` +
|
|
141
|
+
`escaping CONTENT_ROOT (resolves to ${linkReal}, outside ${rootReal})`, { pack: packName, field, offendingValue: rawPath });
|
|
142
|
+
}
|
|
143
|
+
current = linkReal;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a pack record's projection path against CONTENT_ROOT and refuse,
|
|
149
|
+
* fail-closed, any value that: is missing/empty/non-string, carries a null
|
|
150
|
+
* byte, is absolute, violates the pack's schema path pattern, or resolves
|
|
151
|
+
* outside CONTENT_ROOT (a `..` escape). Returns the contained absolute output
|
|
152
|
+
* path. Mirrors deposit/contain.ts segment containment. Refs #2914 / #2904.
|
|
153
|
+
*/
|
|
154
|
+
export function containedOutPath(packName, rawPath, cfg) {
|
|
155
|
+
const field = cfg.path_field ?? "path";
|
|
156
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) {
|
|
157
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} entry has a missing/empty ${field}`, { pack: packName, field, offendingValue: String(rawPath) });
|
|
158
|
+
}
|
|
159
|
+
if (rawPath.includes("\0")) {
|
|
160
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} contains a null byte`, { pack: packName, field, offendingValue: rawPath });
|
|
161
|
+
}
|
|
162
|
+
if (isAbsolute(rawPath) || /^[\\/]/.test(rawPath)) {
|
|
163
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} must be a relative path (got absolute ${JSON.stringify(rawPath)})`, { pack: packName, field, offendingValue: rawPath });
|
|
164
|
+
}
|
|
165
|
+
if (cfg.path_pattern && !cfg.path_pattern.test(rawPath)) {
|
|
166
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} ${JSON.stringify(rawPath)} violates the ` +
|
|
167
|
+
`${packName}-pack schema path pattern ${String(cfg.path_pattern)}`, { pack: packName, field, offendingValue: rawPath });
|
|
168
|
+
}
|
|
169
|
+
// Resolve with realpath semantics on the trusted root, then confirm segment
|
|
170
|
+
// containment. `resolve` collapses `..`, so an escape lands outside rootReal
|
|
171
|
+
// and is refused here even when it slipped past the schema pattern (where `.`
|
|
172
|
+
// can match a path separator).
|
|
173
|
+
const rootReal = safeRealpath(CONTENT_ROOT);
|
|
174
|
+
const outPath = resolve(rootReal, rawPath);
|
|
175
|
+
if (!isContained(rootReal, outPath)) {
|
|
176
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} ${field} ${JSON.stringify(rawPath)} escapes CONTENT_ROOT ` +
|
|
177
|
+
`(resolves to ${outPath}, outside ${rootReal})`, { pack: packName, field, offendingValue: rawPath });
|
|
178
|
+
}
|
|
179
|
+
assertAncestorContained(packName, field, rawPath, rootReal, outPath);
|
|
180
|
+
return outPath;
|
|
181
|
+
}
|
|
182
|
+
/** Refuse a frontmatter field value that embeds a null byte. Refs #2914. */
|
|
183
|
+
function assertNoNullByte(value, packName, field) {
|
|
184
|
+
if (value.includes("\0")) {
|
|
185
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} frontmatter field ${field} contains a null byte`, { pack: packName, field, offendingValue: value });
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Sanitize a single-line YAML frontmatter field (skill `name`): refuse null
|
|
191
|
+
* bytes and any CR/LF. An unescaped newline would inject arbitrary YAML keys or
|
|
192
|
+
* a premature `---` document break into the generated frontmatter. Refs #2914.
|
|
193
|
+
*/
|
|
194
|
+
function sanitizeSingleLineField(value, packName, field) {
|
|
195
|
+
assertNoNullByte(value, packName, field);
|
|
196
|
+
if (/[\r\n]/.test(value)) {
|
|
197
|
+
throw new PackRenderContainmentError(`pack-render refused: ${packName} frontmatter field ${field} must not contain a newline ` +
|
|
198
|
+
`(YAML frontmatter injection)`, { pack: packName, field, offendingValue: value });
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
57
202
|
export const RENDER_REGISTRY = {
|
|
58
203
|
lessons: {
|
|
59
204
|
mode: "collection",
|
|
@@ -73,6 +218,7 @@ export const RENDER_REGISTRY = {
|
|
|
73
218
|
name_field: "id",
|
|
74
219
|
description_field: "description",
|
|
75
220
|
path_field: "path",
|
|
221
|
+
path_pattern: /^skills\/.+\/SKILL\.md$/,
|
|
76
222
|
body_field: "body",
|
|
77
223
|
banner: SKILLS_BANNER,
|
|
78
224
|
},
|
|
@@ -82,6 +228,7 @@ export const RENDER_REGISTRY = {
|
|
|
82
228
|
source: join(CONTENT_ROOT, "packs", "rules", "rules-pack-0.1.json"),
|
|
83
229
|
items_field: "rules",
|
|
84
230
|
path_field: "path",
|
|
231
|
+
path_pattern: /^(?:coding\/.+\.md|AGENTS\.md|main\.md)$/,
|
|
85
232
|
body_field: "body",
|
|
86
233
|
banner: RULES_BANNER,
|
|
87
234
|
},
|
|
@@ -91,6 +238,7 @@ export const RENDER_REGISTRY = {
|
|
|
91
238
|
source: join(CONTENT_ROOT, "packs", "strategies", "strategies-pack-0.1.json"),
|
|
92
239
|
items_field: "strategies",
|
|
93
240
|
path_field: "path",
|
|
241
|
+
path_pattern: /^strategies\/.+\.md$/,
|
|
94
242
|
body_field: "body",
|
|
95
243
|
banner: STRATEGIES_BANNER,
|
|
96
244
|
},
|
|
@@ -100,6 +248,7 @@ export const RENDER_REGISTRY = {
|
|
|
100
248
|
source: join(CONTENT_ROOT, "packs", "patterns", "patterns-pack-0.1.json"),
|
|
101
249
|
items_field: "patterns",
|
|
102
250
|
path_field: "path",
|
|
251
|
+
path_pattern: /^patterns\/.+\.md$/,
|
|
103
252
|
body_field: "body",
|
|
104
253
|
banner: PATTERNS_BANNER,
|
|
105
254
|
},
|
|
@@ -109,6 +258,7 @@ export const RENDER_REGISTRY = {
|
|
|
109
258
|
source: join(CONTENT_ROOT, "packs", "swarm-spec", "swarm-spec-pack-0.1.json"),
|
|
110
259
|
items_field: "entries",
|
|
111
260
|
path_field: "path",
|
|
261
|
+
path_pattern: /^swarm\/.+\.md$/,
|
|
112
262
|
body_field: "body",
|
|
113
263
|
banner: SWARM_SPEC_BANNER,
|
|
114
264
|
},
|
|
@@ -166,12 +316,18 @@ function emitDescription(description) {
|
|
|
166
316
|
return lines.join("\n");
|
|
167
317
|
}
|
|
168
318
|
export function renderSkillDocument(entry, cfg) {
|
|
169
|
-
|
|
170
|
-
|
|
319
|
+
// Sanitize every YAML frontmatter field so a malicious pack cannot inject
|
|
320
|
+
// unescaped frontmatter (newline -> extra key / premature `---`) or a null
|
|
321
|
+
// byte. Refs #2914 / #2904 (skill-pack-supply-01).
|
|
322
|
+
const nameField = cfg.name_field ?? "id";
|
|
323
|
+
const descriptionField = cfg.description_field ?? "description";
|
|
324
|
+
const name = sanitizeSingleLineField(String(entry[nameField]), "skills", nameField);
|
|
325
|
+
const description = assertNoNullByte(String(entry[descriptionField]), "skills", descriptionField);
|
|
171
326
|
const body = entry[cfg.body_field] ?? "";
|
|
172
327
|
const extra = entry.frontmatter_extra;
|
|
173
|
-
const
|
|
174
|
-
const
|
|
328
|
+
const extraStr = typeof extra === "string" ? assertNoNullByte(extra, "skills", "frontmatter_extra") : "";
|
|
329
|
+
const extraBlock = extraStr.length > 0 ? `${extraStr}\n` : "";
|
|
330
|
+
const frontmatter = `---\nname: ${name}\n${emitDescription(description)}\n${extraBlock}---\n`;
|
|
175
331
|
return `${frontmatter}${bannerText(cfg.banner)}\n${String(body)}`;
|
|
176
332
|
}
|
|
177
333
|
export function renderMarkdownDocument(entry, cfg) {
|
|
@@ -182,7 +338,7 @@ const DOCUMENT_RENDERERS = {
|
|
|
182
338
|
skill: renderSkillDocument,
|
|
183
339
|
markdown: renderMarkdownDocument,
|
|
184
340
|
};
|
|
185
|
-
function targetsForPack(pack, cfg) {
|
|
341
|
+
export function targetsForPack(packName, pack, cfg) {
|
|
186
342
|
if (cfg.mode === "collection") {
|
|
187
343
|
return [[cfg.output ?? "", renderCollection(pack, cfg)]];
|
|
188
344
|
}
|
|
@@ -203,7 +359,10 @@ function targetsForPack(pack, cfg) {
|
|
|
203
359
|
const pathField = cfg.path_field ?? "path";
|
|
204
360
|
// Document projection targets (skills/rules/strategies/patterns) are
|
|
205
361
|
// shippable content -- they live under content/ in the source repo (#1875).
|
|
206
|
-
|
|
362
|
+
// #2914: contain the pack-supplied path under CONTENT_ROOT and enforce the
|
|
363
|
+
// pack schema path pattern BEFORE render so a malicious pack JSON cannot
|
|
364
|
+
// write outside content/ via an absolute / `..` / null-byte path.
|
|
365
|
+
const outPath = containedOutPath(packName, record[pathField], cfg);
|
|
207
366
|
targets.push([outPath, docRenderer(record, cfg)]);
|
|
208
367
|
}
|
|
209
368
|
return targets;
|
|
@@ -218,7 +377,7 @@ export function collectTargets(packFilter) {
|
|
|
218
377
|
throw new Error(`pack source not found: ${cfg.source}`);
|
|
219
378
|
}
|
|
220
379
|
const pack = JSON.parse(readFileSync(cfg.source, "utf8"));
|
|
221
|
-
for (const [outPath, text] of targetsForPack(pack, cfg)) {
|
|
380
|
+
for (const [outPath, text] of targetsForPack(name, pack, cfg)) {
|
|
222
381
|
targets.push([name, outPath, text]);
|
|
223
382
|
}
|
|
224
383
|
}
|
|
@@ -236,8 +395,8 @@ export function renderFile(source) {
|
|
|
236
395
|
}
|
|
237
396
|
export function writeRender(source, output) {
|
|
238
397
|
const text = renderFile(source);
|
|
239
|
-
|
|
240
|
-
|
|
398
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
399
|
+
writeContainedFile(output, text);
|
|
241
400
|
return text;
|
|
242
401
|
}
|
|
243
402
|
export function checkDrift(source, output) {
|
|
@@ -295,9 +454,9 @@ function runMultipack(packFilter, check) {
|
|
|
295
454
|
process.stdout.write(`pack-drift: all ${targets.length} projection(s) in sync.\n`);
|
|
296
455
|
return 0;
|
|
297
456
|
}
|
|
457
|
+
// #2980 wave D: product write sinks route through containedWrite.
|
|
298
458
|
for (const [_name, outPath, text] of targets) {
|
|
299
|
-
|
|
300
|
-
writeFileSync(outPath, text, "utf8");
|
|
459
|
+
writeContainedFile(outPath, text);
|
|
301
460
|
}
|
|
302
461
|
process.stdout.write(`Rendered ${targets.length} projection(s) across ${Object.keys(RENDER_REGISTRY).length} pack(s).\n`);
|
|
303
462
|
return 0;
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
export declare const SUSPICIOUS_TOKENS: readonly string[];
|
|
3
3
|
export declare const QUARANTINE_FENCE_OPEN = "```quarantined";
|
|
4
4
|
export declare const QUARANTINE_FENCE_CLOSE = "```";
|
|
5
|
+
/**
|
|
6
|
+
* Neutralize a body line so it cannot act as a CommonMark fence open/close.
|
|
7
|
+
* Used on content placed inside a ```quarantined wrapper (#2915 / cache-quarantine-03).
|
|
8
|
+
*
|
|
9
|
+
* A nested bare ``` (or longer run, optional ≤3-space indent) would otherwise
|
|
10
|
+
* early-close the outer fence and let following attacker text render outside
|
|
11
|
+
* the quarantined info-string. Break the delimiter run by inserting a backslash
|
|
12
|
+
* after the first fence character: ``` → `\`` , ~~~ → ~\~~ .
|
|
13
|
+
*/
|
|
14
|
+
export declare function neutralizeFenceLine(line: string): string;
|
|
5
15
|
export declare function quarantineBody(rawMd: string): string;
|
|
6
16
|
export declare function main(argv?: string[]): number;
|
|
7
17
|
//# sourceMappingURL=quarantine-ext.d.ts.map
|
|
@@ -28,6 +28,26 @@ export const SUSPICIOUS_TOKENS = [
|
|
|
28
28
|
];
|
|
29
29
|
export const QUARANTINE_FENCE_OPEN = "```quarantined";
|
|
30
30
|
export const QUARANTINE_FENCE_CLOSE = "```";
|
|
31
|
+
/**
|
|
32
|
+
* Neutralize a body line so it cannot act as a CommonMark fence open/close.
|
|
33
|
+
* Used on content placed inside a ```quarantined wrapper (#2915 / cache-quarantine-03).
|
|
34
|
+
*
|
|
35
|
+
* A nested bare ``` (or longer run, optional ≤3-space indent) would otherwise
|
|
36
|
+
* early-close the outer fence and let following attacker text render outside
|
|
37
|
+
* the quarantined info-string. Break the delimiter run by inserting a backslash
|
|
38
|
+
* after the first fence character: ``` → `\`` , ~~~ → ~\~~ .
|
|
39
|
+
*/
|
|
40
|
+
export function neutralizeFenceLine(line) {
|
|
41
|
+
const match = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
42
|
+
if (match === null)
|
|
43
|
+
return line;
|
|
44
|
+
const indent = match[1] ?? "";
|
|
45
|
+
const delim = match[2] ?? "";
|
|
46
|
+
const rest = match[3] ?? "";
|
|
47
|
+
// One backslash after the first fence char breaks the delimiter run.
|
|
48
|
+
// Concat (not a template literal) so "\\" is unambiguously a single "\".
|
|
49
|
+
return indent + delim[0] + "\\" + delim.slice(1) + rest;
|
|
50
|
+
}
|
|
31
51
|
function isWordChar(ch) {
|
|
32
52
|
return ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_");
|
|
33
53
|
}
|
|
@@ -133,14 +153,18 @@ export function quarantineBody(rawMd) {
|
|
|
133
153
|
sectionEnd += 1;
|
|
134
154
|
}
|
|
135
155
|
out.push(QUARANTINE_FENCE_OPEN);
|
|
136
|
-
|
|
156
|
+
// #2915: neutralize nested fence lines so they cannot early-close.
|
|
157
|
+
for (const bodyLine of lines.slice(i, sectionEnd)) {
|
|
158
|
+
out.push(neutralizeFenceLine(bodyLine));
|
|
159
|
+
}
|
|
137
160
|
out.push(QUARANTINE_FENCE_CLOSE);
|
|
138
161
|
i = sectionEnd;
|
|
139
162
|
continue;
|
|
140
163
|
}
|
|
141
164
|
if (isSuspicious(line)) {
|
|
142
165
|
out.push(QUARANTINE_FENCE_OPEN);
|
|
143
|
-
|
|
166
|
+
// #2915: neutralize in case the single line itself is fence-shaped.
|
|
167
|
+
out.push(neutralizeFenceLine(line));
|
|
144
168
|
out.push(QUARANTINE_FENCE_CLOSE);
|
|
145
169
|
i += 1;
|
|
146
170
|
continue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { PLAN_SEQUENCE_FILENAME, parsePlanSequence } from "./types.js";
|
|
4
5
|
export function planSequencePath(projectRoot) {
|
|
5
6
|
return join(projectRoot, ".deft", PLAN_SEQUENCE_FILENAME);
|
|
@@ -14,8 +15,14 @@ export function readPlanSequence(projectRoot) {
|
|
|
14
15
|
}
|
|
15
16
|
export function writePlanSequence(projectRoot, sequence) {
|
|
16
17
|
const path = planSequencePath(projectRoot);
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
19
|
+
const root = resolve(projectRoot);
|
|
20
|
+
containedWrite({
|
|
21
|
+
root,
|
|
22
|
+
target: path,
|
|
23
|
+
data: `${JSON.stringify(sequence, null, 2)}\n`,
|
|
24
|
+
mode: "replace",
|
|
25
|
+
});
|
|
19
26
|
return path;
|
|
20
27
|
}
|
|
21
28
|
export function clearPlanSequence(projectRoot) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed hotfix classifier (#1193 R2 / Wave 2 of #2948).
|
|
3
|
+
*
|
|
4
|
+
* Hard structural predicates — not agent judgement. Agent may propose the
|
|
5
|
+
* label `hotfix-candidate` only; a human promotes to `hotfix`. Pipeline keys
|
|
6
|
+
* off `hotfix` exclusively; `hotfix-candidate` has zero pipeline meaning.
|
|
7
|
+
*/
|
|
8
|
+
import type { HotfixCriteria } from "@deftai/directive-types";
|
|
9
|
+
export declare const FIELD_HOTFIX_CRITERIA = "plan.policy.hotfixCriteria";
|
|
10
|
+
export declare const FIELD_HOTFIX_CRITERIA_CLI_ALIAS = "hotfixCriteria";
|
|
11
|
+
/** Agent-proposed beacon only — never pipeline-authorizing. */
|
|
12
|
+
export declare const HOTFIX_CANDIDATE_LABEL: "hotfix-candidate";
|
|
13
|
+
/** Human-promoted label; deploy pipeline keys off this exclusively. */
|
|
14
|
+
export declare const HOTFIX_LABEL: "hotfix";
|
|
15
|
+
export declare const DEFAULT_HOTFIX_MAX_LINES = 10;
|
|
16
|
+
export declare const DEFAULT_HOTFIX_MAX_FILES = 2;
|
|
17
|
+
/** Paths that never qualify as hotfix (deploy / CI / migrations / secrets). */
|
|
18
|
+
export declare const DEFAULT_FORBIDDEN_PATH_GLOBS: readonly string[];
|
|
19
|
+
export declare const DEFAULT_HOTFIX_CRITERIA: Required<HotfixCriteria>;
|
|
20
|
+
export interface HotfixEligibilityInput {
|
|
21
|
+
/** Total lines added+removed (or equivalent structural line count). */
|
|
22
|
+
readonly linesChanged: number;
|
|
23
|
+
/** Distinct files touched. */
|
|
24
|
+
readonly filesChanged: number;
|
|
25
|
+
/** Project-relative POSIX paths in the diff. */
|
|
26
|
+
readonly paths: readonly string[];
|
|
27
|
+
/** Pure revert of a prior change — always eligible. */
|
|
28
|
+
readonly isPureRevert?: boolean;
|
|
29
|
+
/** One-character / trivial edit signal (always eligible when true with restoresGreen). */
|
|
30
|
+
readonly isOneCharacterEdit?: boolean;
|
|
31
|
+
/** Refactors of any size are never hotfix. */
|
|
32
|
+
readonly isRefactor?: boolean;
|
|
33
|
+
/** New package / dependency entries. */
|
|
34
|
+
readonly addsNewDependency?: boolean;
|
|
35
|
+
/** Adds, removes, or renames an exported symbol/surface. */
|
|
36
|
+
readonly changesExportedSurface?: boolean;
|
|
37
|
+
/** Schema or migration changes. */
|
|
38
|
+
readonly touchesSchemaOrMigration?: boolean;
|
|
39
|
+
/** New handler/route/media-type surface. */
|
|
40
|
+
readonly addsNewHandlerOrRoute?: boolean;
|
|
41
|
+
/** Build was green on main; this change restores green. */
|
|
42
|
+
readonly restoresGreen?: boolean;
|
|
43
|
+
/** Criteria override (defaults + project typed policy). */
|
|
44
|
+
readonly criteria?: HotfixCriteria | null;
|
|
45
|
+
}
|
|
46
|
+
export type HotfixDenyReason = "refactor" | "new-export" | "new-dependency" | "schema-migration" | "new-handler-route" | "forbidden-path" | "too-many-lines" | "too-many-files" | "not-restoring-green";
|
|
47
|
+
export interface HotfixEligibilityResult {
|
|
48
|
+
readonly eligible: boolean;
|
|
49
|
+
/** When eligible, the agent may apply this label only. */
|
|
50
|
+
readonly proposedLabel: typeof HOTFIX_CANDIDATE_LABEL | null;
|
|
51
|
+
readonly reasons: readonly string[];
|
|
52
|
+
readonly denyCodes: readonly HotfixDenyReason[];
|
|
53
|
+
readonly criteria: Required<HotfixCriteria>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pure hotfix eligibility evaluator (#1193 R2).
|
|
57
|
+
*
|
|
58
|
+
* Eligible (agent may propose `hotfix-candidate`):
|
|
59
|
+
* - pure revert always
|
|
60
|
+
* - one-character edit that restores green
|
|
61
|
+
* - small fix: ≤ maxLines AND ≤ maxFiles, no new deps/exports/schema, no forbidden
|
|
62
|
+
* paths, not a refactor, restores green
|
|
63
|
+
*
|
|
64
|
+
* Never eligible: refactors, new handlers/routes, export surface changes, forbidden paths.
|
|
65
|
+
*/
|
|
66
|
+
export declare function evaluateHotfixEligibility(input: HotfixEligibilityInput): HotfixEligibilityResult;
|
|
67
|
+
/** Resolve typed hotfixCriteria from a raw policy block value. */
|
|
68
|
+
export declare function resolveHotfixCriteria(raw: unknown): Required<HotfixCriteria>;
|
|
69
|
+
export interface HotfixCriteriaPolicyField {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly current: Required<HotfixCriteria>;
|
|
72
|
+
readonly default: Required<HotfixCriteria>;
|
|
73
|
+
readonly source: string;
|
|
74
|
+
}
|
|
75
|
+
/** Inspector row for `policy:show --field=hotfixCriteria`. */
|
|
76
|
+
export declare function inspectHotfixCriteria(data: Record<string, unknown> | null): HotfixCriteriaPolicyField;
|
|
77
|
+
/** Load hotfix criteria from project root. */
|
|
78
|
+
export declare function loadHotfixCriteriaFromProject(projectRoot: string): Required<HotfixCriteria>;
|
|
79
|
+
//# sourceMappingURL=hotfix-criteria.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed hotfix classifier (#1193 R2 / Wave 2 of #2948).
|
|
3
|
+
*
|
|
4
|
+
* Hard structural predicates — not agent judgement. Agent may propose the
|
|
5
|
+
* label `hotfix-candidate` only; a human promotes to `hotfix`. Pipeline keys
|
|
6
|
+
* off `hotfix` exclusively; `hotfix-candidate` has zero pipeline meaning.
|
|
7
|
+
*/
|
|
8
|
+
import { matchAny } from "../orchestration/pathspec.js";
|
|
9
|
+
import { readPlanPolicy } from "./plan-extensions.js";
|
|
10
|
+
import { loadProjectDefinition } from "./resolve.js";
|
|
11
|
+
export const FIELD_HOTFIX_CRITERIA = "plan.policy.hotfixCriteria";
|
|
12
|
+
export const FIELD_HOTFIX_CRITERIA_CLI_ALIAS = "hotfixCriteria";
|
|
13
|
+
/** Agent-proposed beacon only — never pipeline-authorizing. */
|
|
14
|
+
export const HOTFIX_CANDIDATE_LABEL = "hotfix-candidate";
|
|
15
|
+
/** Human-promoted label; deploy pipeline keys off this exclusively. */
|
|
16
|
+
export const HOTFIX_LABEL = "hotfix";
|
|
17
|
+
export const DEFAULT_HOTFIX_MAX_LINES = 10;
|
|
18
|
+
export const DEFAULT_HOTFIX_MAX_FILES = 2;
|
|
19
|
+
/** Paths that never qualify as hotfix (deploy / CI / migrations / secrets). */
|
|
20
|
+
export const DEFAULT_FORBIDDEN_PATH_GLOBS = [
|
|
21
|
+
"Dockerfile",
|
|
22
|
+
"**/Dockerfile",
|
|
23
|
+
"fly.toml",
|
|
24
|
+
"**/fly.toml",
|
|
25
|
+
".github/workflows/**",
|
|
26
|
+
"migrations/**",
|
|
27
|
+
"**/migrations/**",
|
|
28
|
+
"**/*secret*",
|
|
29
|
+
"**/*billing*",
|
|
30
|
+
"**/.env",
|
|
31
|
+
"**/.env.*",
|
|
32
|
+
"**/auth/**",
|
|
33
|
+
"**/secrets/**",
|
|
34
|
+
];
|
|
35
|
+
export const DEFAULT_HOTFIX_CRITERIA = {
|
|
36
|
+
maxLines: DEFAULT_HOTFIX_MAX_LINES,
|
|
37
|
+
maxFiles: DEFAULT_HOTFIX_MAX_FILES,
|
|
38
|
+
forbiddenPathGlobs: DEFAULT_FORBIDDEN_PATH_GLOBS,
|
|
39
|
+
};
|
|
40
|
+
function normalizeCriteria(raw) {
|
|
41
|
+
const maxLines = typeof raw?.maxLines === "number" && Number.isInteger(raw.maxLines) && raw.maxLines >= 0
|
|
42
|
+
? raw.maxLines
|
|
43
|
+
: DEFAULT_HOTFIX_MAX_LINES;
|
|
44
|
+
const maxFiles = typeof raw?.maxFiles === "number" && Number.isInteger(raw.maxFiles) && raw.maxFiles >= 0
|
|
45
|
+
? raw.maxFiles
|
|
46
|
+
: DEFAULT_HOTFIX_MAX_FILES;
|
|
47
|
+
const forbidden = Array.isArray(raw?.forbiddenPathGlobs) && raw.forbiddenPathGlobs.length > 0
|
|
48
|
+
? raw.forbiddenPathGlobs.filter((g) => typeof g === "string" && g.length > 0)
|
|
49
|
+
: DEFAULT_FORBIDDEN_PATH_GLOBS;
|
|
50
|
+
return { maxLines, maxFiles, forbiddenPathGlobs: forbidden };
|
|
51
|
+
}
|
|
52
|
+
function pathIsForbidden(path, globs) {
|
|
53
|
+
const posix = path.replace(/\\/g, "/");
|
|
54
|
+
// Basename-only defaults (Dockerfile, fly.toml) match any depth.
|
|
55
|
+
const base = posix.includes("/") ? posix.slice(posix.lastIndexOf("/") + 1) : posix;
|
|
56
|
+
for (const g of globs) {
|
|
57
|
+
if (!g.includes("/") && !g.includes("*") && base === g)
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return matchAny(globs, posix);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pure hotfix eligibility evaluator (#1193 R2).
|
|
64
|
+
*
|
|
65
|
+
* Eligible (agent may propose `hotfix-candidate`):
|
|
66
|
+
* - pure revert always
|
|
67
|
+
* - one-character edit that restores green
|
|
68
|
+
* - small fix: ≤ maxLines AND ≤ maxFiles, no new deps/exports/schema, no forbidden
|
|
69
|
+
* paths, not a refactor, restores green
|
|
70
|
+
*
|
|
71
|
+
* Never eligible: refactors, new handlers/routes, export surface changes, forbidden paths.
|
|
72
|
+
*/
|
|
73
|
+
export function evaluateHotfixEligibility(input) {
|
|
74
|
+
const criteria = normalizeCriteria(input.criteria);
|
|
75
|
+
const denyCodes = [];
|
|
76
|
+
const reasons = [];
|
|
77
|
+
if (input.isPureRevert === true) {
|
|
78
|
+
return {
|
|
79
|
+
eligible: true,
|
|
80
|
+
proposedLabel: HOTFIX_CANDIDATE_LABEL,
|
|
81
|
+
reasons: ["pure-revert always qualifies as hotfix-candidate"],
|
|
82
|
+
denyCodes: [],
|
|
83
|
+
criteria,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (input.isOneCharacterEdit === true && input.restoresGreen !== false) {
|
|
87
|
+
return {
|
|
88
|
+
eligible: true,
|
|
89
|
+
proposedLabel: HOTFIX_CANDIDATE_LABEL,
|
|
90
|
+
reasons: ["one-character edit that restores green qualifies as hotfix-candidate"],
|
|
91
|
+
denyCodes: [],
|
|
92
|
+
criteria,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (input.isRefactor === true) {
|
|
96
|
+
denyCodes.push("refactor");
|
|
97
|
+
reasons.push("refactors of any size are never hotfix");
|
|
98
|
+
}
|
|
99
|
+
if (input.changesExportedSurface === true) {
|
|
100
|
+
denyCodes.push("new-export");
|
|
101
|
+
reasons.push("exported surface add/remove/rename is never hotfix");
|
|
102
|
+
}
|
|
103
|
+
if (input.addsNewDependency === true) {
|
|
104
|
+
denyCodes.push("new-dependency");
|
|
105
|
+
reasons.push("new dependencies are never hotfix");
|
|
106
|
+
}
|
|
107
|
+
if (input.touchesSchemaOrMigration === true) {
|
|
108
|
+
denyCodes.push("schema-migration");
|
|
109
|
+
reasons.push("schema/migration changes are never hotfix");
|
|
110
|
+
}
|
|
111
|
+
if (input.addsNewHandlerOrRoute === true) {
|
|
112
|
+
denyCodes.push("new-handler-route");
|
|
113
|
+
reasons.push("new handlers/routes/media types are never hotfix");
|
|
114
|
+
}
|
|
115
|
+
for (const p of input.paths) {
|
|
116
|
+
if (pathIsForbidden(p, criteria.forbiddenPathGlobs)) {
|
|
117
|
+
denyCodes.push("forbidden-path");
|
|
118
|
+
reasons.push(`forbidden path: ${p}`);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (input.linesChanged > criteria.maxLines) {
|
|
123
|
+
denyCodes.push("too-many-lines");
|
|
124
|
+
reasons.push(`linesChanged ${input.linesChanged} > maxLines ${criteria.maxLines}`);
|
|
125
|
+
}
|
|
126
|
+
if (input.filesChanged > criteria.maxFiles) {
|
|
127
|
+
denyCodes.push("too-many-files");
|
|
128
|
+
reasons.push(`filesChanged ${input.filesChanged} > maxFiles ${criteria.maxFiles}`);
|
|
129
|
+
}
|
|
130
|
+
// Small-fix path requires restoring green unless pure-revert/one-char already returned.
|
|
131
|
+
if (input.restoresGreen === false) {
|
|
132
|
+
denyCodes.push("not-restoring-green");
|
|
133
|
+
reasons.push("change does not restore green on main");
|
|
134
|
+
}
|
|
135
|
+
if (denyCodes.length > 0) {
|
|
136
|
+
return {
|
|
137
|
+
eligible: false,
|
|
138
|
+
proposedLabel: null,
|
|
139
|
+
reasons,
|
|
140
|
+
denyCodes,
|
|
141
|
+
criteria,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
eligible: true,
|
|
146
|
+
proposedLabel: HOTFIX_CANDIDATE_LABEL,
|
|
147
|
+
reasons: [
|
|
148
|
+
`small fix within limits (≤${criteria.maxLines} lines, ≤${criteria.maxFiles} files); ` +
|
|
149
|
+
"agent may apply hotfix-candidate only — human promotes hotfix",
|
|
150
|
+
],
|
|
151
|
+
denyCodes: [],
|
|
152
|
+
criteria,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Resolve typed hotfixCriteria from a raw policy block value. */
|
|
156
|
+
export function resolveHotfixCriteria(raw) {
|
|
157
|
+
if (raw === null || raw === undefined)
|
|
158
|
+
return { ...DEFAULT_HOTFIX_CRITERIA };
|
|
159
|
+
if (typeof raw !== "object" || Array.isArray(raw))
|
|
160
|
+
return { ...DEFAULT_HOTFIX_CRITERIA };
|
|
161
|
+
return normalizeCriteria(raw);
|
|
162
|
+
}
|
|
163
|
+
/** Inspector row for `policy:show --field=hotfixCriteria`. */
|
|
164
|
+
export function inspectHotfixCriteria(data) {
|
|
165
|
+
const defaults = { ...DEFAULT_HOTFIX_CRITERIA };
|
|
166
|
+
if (data === null) {
|
|
167
|
+
return { name: FIELD_HOTFIX_CRITERIA, current: defaults, default: defaults, source: "default" };
|
|
168
|
+
}
|
|
169
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
170
|
+
if (typeof policyBlock !== "object" ||
|
|
171
|
+
policyBlock === null ||
|
|
172
|
+
Array.isArray(policyBlock) ||
|
|
173
|
+
!("hotfixCriteria" in policyBlock)) {
|
|
174
|
+
return { name: FIELD_HOTFIX_CRITERIA, current: defaults, default: defaults, source: "default" };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
name: FIELD_HOTFIX_CRITERIA,
|
|
178
|
+
current: resolveHotfixCriteria(policyBlock.hotfixCriteria),
|
|
179
|
+
default: defaults,
|
|
180
|
+
source: "typed",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** Load hotfix criteria from project root. */
|
|
184
|
+
export function loadHotfixCriteriaFromProject(projectRoot) {
|
|
185
|
+
const [data] = loadProjectDefinition(projectRoot);
|
|
186
|
+
if (data === null)
|
|
187
|
+
return { ...DEFAULT_HOTFIX_CRITERIA };
|
|
188
|
+
const policyBlock = readPlanPolicy(data.plan);
|
|
189
|
+
if (typeof policyBlock !== "object" ||
|
|
190
|
+
policyBlock === null ||
|
|
191
|
+
Array.isArray(policyBlock) ||
|
|
192
|
+
!("hotfixCriteria" in policyBlock)) {
|
|
193
|
+
return { ...DEFAULT_HOTFIX_CRITERIA };
|
|
194
|
+
}
|
|
195
|
+
return resolveHotfixCriteria(policyBlock.hotfixCriteria);
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=hotfix-criteria.js.map
|
package/dist/policy/index.d.ts
CHANGED
|
@@ -4,16 +4,21 @@ export * from "./capacity.js";
|
|
|
4
4
|
export * from "./decisions.js";
|
|
5
5
|
export * from "./disclosure.js";
|
|
6
6
|
export * from "./host-hooks.js";
|
|
7
|
+
export * from "./hotfix-criteria.js";
|
|
8
|
+
export * from "./intent-ceiling.js";
|
|
9
|
+
export * from "./no-deft-directive.js";
|
|
7
10
|
export * from "./org-force-on-migration.js";
|
|
8
11
|
export * from "./plan-extensions.js";
|
|
9
12
|
export * from "./policy-invocation.js";
|
|
10
13
|
export * from "./product-signal.js";
|
|
14
|
+
export * from "./require-human-merge.js";
|
|
11
15
|
export * from "./resolve.js";
|
|
12
16
|
export * from "./runtime-authority.js";
|
|
13
17
|
export * from "./staleness-tickler.js";
|
|
14
18
|
export * from "./value-feedback.js";
|
|
15
19
|
export * from "./value-feedback-autoenable.js";
|
|
16
20
|
export * from "./wip.js";
|
|
21
|
+
export * from "./write-fence.js";
|
|
17
22
|
export declare const FIELD_ALLOW_DIRECT_COMMITS = "plan.policy.allowDirectCommitsToMaster";
|
|
18
23
|
export declare const FIELD_WIP_CAP = "plan.policy.wipCap";
|
|
19
24
|
export declare const FIELD_SESSION_RITUAL_STALENESS_HOURS = "plan.policy.sessionRitualStalenessHours";
|