@deftai/directive-core 0.74.0 → 0.76.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/agents-md-budget/evaluate.d.ts +61 -0
- package/dist/agents-md-budget/evaluate.js +341 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +35 -0
- package/dist/agents-md-budget/skill-frontmatter.js +116 -0
- package/dist/cache/operations.d.ts +2 -0
- package/dist/cache/operations.js +16 -1
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- package/dist/init-deposit/scaffold.js +35 -20
- package/dist/intake/issue-emit.js +2 -2
- package/dist/intake/issue-ingest.js +23 -4
- package/dist/packaging/openpackage-tiers.d.ts +12 -0
- package/dist/packaging/openpackage-tiers.js +42 -0
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- package/dist/preflight/evaluate.d.ts +5 -2
- package/dist/preflight/evaluate.js +9 -4
- package/dist/preflight/index.d.ts +1 -1
- package/dist/preflight/index.js +1 -1
- package/dist/release/pipeline-fixture.d.ts +3 -0
- package/dist/release/pipeline-fixture.js +11 -0
- package/dist/release/pipeline.js +4 -0
- package/dist/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- package/dist/scope/transition.js +17 -2
- package/dist/scope/vbrief-ref.d.ts +6 -0
- package/dist/scope/vbrief-ref.js +23 -4
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/posture.d.ts +50 -0
- package/dist/session/posture.js +152 -0
- package/dist/session/ritual-sentinel.d.ts +2 -0
- package/dist/session/ritual-sentinel.js +3 -0
- package/dist/session/session-start.d.ts +8 -0
- package/dist/session/session-start.js +43 -0
- package/dist/session/verify-session-ritual.d.ts +6 -0
- package/dist/session/verify-session-ritual.js +67 -4
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/bootstrap/gitignore.js +8 -4
- package/dist/triage/cache-path.js +16 -3
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/triage/welcome/writers.js +2 -0
- package/dist/ts-check-lane/index.d.ts +1 -1
- package/dist/ts-check-lane/index.js +1 -1
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +15 -0
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/vbrief-build/constants.d.ts +3 -3
- package/dist/vbrief-build/constants.js +4 -3
- package/dist/vbrief-build/index.d.ts +1 -1
- package/dist/vbrief-build/index.js +1 -1
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/package.json +7 -3
|
@@ -10,12 +10,19 @@ import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
|
10
10
|
import { platform } from "node:os";
|
|
11
11
|
import { dirname, join, relative } from "node:path";
|
|
12
12
|
import { copyTree } from "../deposit/copy-tree.js";
|
|
13
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
13
14
|
import { agentsRefreshPlan } from "../platform/agents-md.js";
|
|
14
15
|
import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
|
|
15
16
|
import { CANONICAL_INSTALL_ROOT } from "./constants.js";
|
|
16
17
|
import { installerManagedGuardEre } from "./hygiene.js";
|
|
17
18
|
export { CANONICAL_INSTALL_ROOT };
|
|
18
19
|
export const CORE_GLOB = ".deft/core/**";
|
|
20
|
+
/** Refuse init/update projection writes that escape via repo-controlled symlinks (#2446). */
|
|
21
|
+
function projectionTarget(projectDir, ...relSegments) {
|
|
22
|
+
const target = join(projectDir, ...relSegments);
|
|
23
|
+
assertProjectionContained(projectDir, target);
|
|
24
|
+
return target;
|
|
25
|
+
}
|
|
19
26
|
const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
|
|
20
27
|
const CORE_GUARD_WORKFLOW_REL = ".github/workflows/deft-core-guard.yml";
|
|
21
28
|
const FRAMEWORK_SELF_TEST_REL = ".deft/core/tests";
|
|
@@ -232,7 +239,7 @@ export const PIN_DEPENDENCY_NAME = "@deftai/directive";
|
|
|
232
239
|
*/
|
|
233
240
|
export function ensurePackageJsonPin(projectDir, version, io) {
|
|
234
241
|
const pinVersion = version.trim().replace(/^v/i, "");
|
|
235
|
-
const path =
|
|
242
|
+
const path = projectionTarget(projectDir, "package.json");
|
|
236
243
|
const existed = existsSync(path);
|
|
237
244
|
let pkg = {};
|
|
238
245
|
if (existed) {
|
|
@@ -284,7 +291,7 @@ export function writeAgentsMd(projectDir, deftDir, io) {
|
|
|
284
291
|
if (typeof newContent !== "string") {
|
|
285
292
|
throw new Error("AGENTS.md render produced no content");
|
|
286
293
|
}
|
|
287
|
-
const path =
|
|
294
|
+
const path = projectionTarget(projectDir, "AGENTS.md");
|
|
288
295
|
writeFileSync(path, newContent, "utf8");
|
|
289
296
|
if (state === "absent") {
|
|
290
297
|
io.printf("AGENTS.md created.\n");
|
|
@@ -294,9 +301,9 @@ export function writeAgentsMd(projectDir, deftDir, io) {
|
|
|
294
301
|
}
|
|
295
302
|
return true;
|
|
296
303
|
}
|
|
297
|
-
async function ensureVbriefLifecycleDirs(
|
|
304
|
+
async function ensureVbriefLifecycleDirs(projectDir) {
|
|
298
305
|
for (const sub of VBRIEF_LIFECYCLE_DIRS) {
|
|
299
|
-
const dir =
|
|
306
|
+
const dir = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, sub);
|
|
300
307
|
await mkdir(dir, { recursive: true, mode: 0o755 });
|
|
301
308
|
const gitkeep = join(dir, ".gitkeep");
|
|
302
309
|
try {
|
|
@@ -323,9 +330,9 @@ function vbriefLifecycleDirsPresent(consumerVbrief) {
|
|
|
323
330
|
});
|
|
324
331
|
}
|
|
325
332
|
export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
326
|
-
const consumerVbrief =
|
|
327
|
-
const schemasDst =
|
|
328
|
-
const vbriefMdDst =
|
|
333
|
+
const consumerVbrief = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR);
|
|
334
|
+
const schemasDst = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, "schemas");
|
|
335
|
+
const vbriefMdDst = projectionTarget(projectDir, MIGRATED_ARTIFACT_DIR, "vbrief.md");
|
|
329
336
|
const schemasPresent = existsSync(schemasDst) && statSync(schemasDst).isDirectory();
|
|
330
337
|
const vbriefMdPresent = existsSync(vbriefMdDst) && statSync(vbriefMdDst).isFile();
|
|
331
338
|
const lifecyclePresent = vbriefLifecycleDirsPresent(consumerVbrief);
|
|
@@ -352,20 +359,21 @@ export async function writeConsumerVbrief(projectDir, deftDir, io) {
|
|
|
352
359
|
writeFileSync(vbriefMdDst, VBRIEF_README_BODY, "utf8");
|
|
353
360
|
}
|
|
354
361
|
}
|
|
355
|
-
await ensureVbriefLifecycleDirs(
|
|
362
|
+
await ensureVbriefLifecycleDirs(projectDir);
|
|
356
363
|
io.printf("vbrief/ deposited at project root (schemas + vbrief.md + lifecycle dirs).\n");
|
|
357
364
|
return true;
|
|
358
365
|
}
|
|
359
366
|
export function writeAgentsSkills(projectDir, io) {
|
|
367
|
+
projectionTarget(projectDir, ".agents");
|
|
360
368
|
const allExist = AGENTS_SKILLS.every((skill) => existsSync(join(projectDir, ".agents", "skills", skill.dir, "SKILL.md")));
|
|
361
369
|
if (allExist) {
|
|
362
370
|
io.printf(".agents/skills/ already present — skipping.\n");
|
|
363
371
|
return false;
|
|
364
372
|
}
|
|
365
373
|
for (const skill of AGENTS_SKILLS) {
|
|
366
|
-
const dir =
|
|
374
|
+
const dir = projectionTarget(projectDir, ".agents", "skills", skill.dir);
|
|
367
375
|
mkdirSync(dir, { recursive: true });
|
|
368
|
-
const path =
|
|
376
|
+
const path = projectionTarget(projectDir, ".agents", "skills", skill.dir, "SKILL.md");
|
|
369
377
|
if (existsSync(path))
|
|
370
378
|
continue;
|
|
371
379
|
writeFileSync(path, skill.content, "utf8");
|
|
@@ -412,7 +420,7 @@ function insertDeftIncludeAfterIncludesLine(content) {
|
|
|
412
420
|
return { content, ok: false };
|
|
413
421
|
}
|
|
414
422
|
export function ensureTaskfile(projectDir, io) {
|
|
415
|
-
const path =
|
|
423
|
+
const path = projectionTarget(projectDir, "Taskfile.yml");
|
|
416
424
|
let existing = "";
|
|
417
425
|
if (existsSync(path)) {
|
|
418
426
|
existing = readFileSync(path, "utf8");
|
|
@@ -457,28 +465,32 @@ export function ensureTaskfile(projectDir, io) {
|
|
|
457
465
|
return true;
|
|
458
466
|
}
|
|
459
467
|
const HOOK_FILENAMES = ["pre-commit", "pre-push"];
|
|
468
|
+
const HOOK_SUPPORT_FILENAMES = ["_deft-run.sh"];
|
|
460
469
|
const HOOK_FILE_MODE = 0o755;
|
|
470
|
+
const HOOK_SUPPORT_FILE_MODE = 0o644;
|
|
461
471
|
export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
462
472
|
const srcDir = join(deftDir, ".githooks");
|
|
463
473
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
464
474
|
io.printf(`git hooks source ${srcDir} absent — skipping hook wiring.\n`);
|
|
465
475
|
return false;
|
|
466
476
|
}
|
|
467
|
-
const dstDir =
|
|
477
|
+
const dstDir = projectionTarget(projectDir, ".githooks");
|
|
468
478
|
mkdirSync(dstDir, { recursive: true });
|
|
469
479
|
let filesDeposited = false;
|
|
470
|
-
for (const name of HOOK_FILENAMES) {
|
|
480
|
+
for (const name of [...HOOK_FILENAMES, ...HOOK_SUPPORT_FILENAMES]) {
|
|
471
481
|
const src = join(srcDir, name);
|
|
472
482
|
if (!existsSync(src))
|
|
473
483
|
continue;
|
|
474
484
|
const data = readFileSync(src);
|
|
475
|
-
const dst =
|
|
485
|
+
const dst = projectionTarget(projectDir, ".githooks", name);
|
|
476
486
|
const existing = existsSync(dst) ? readFileSync(dst) : null;
|
|
487
|
+
const isHookScript = HOOK_FILENAMES.includes(name);
|
|
477
488
|
if (!existing?.equals(data)) {
|
|
478
|
-
|
|
489
|
+
const mode = isHookScript ? HOOK_FILE_MODE : HOOK_SUPPORT_FILE_MODE;
|
|
490
|
+
writeFileSync(dst, data, { mode });
|
|
479
491
|
filesDeposited = true;
|
|
480
492
|
}
|
|
481
|
-
if (platform() !== "win32") {
|
|
493
|
+
if (platform() !== "win32" && isHookScript) {
|
|
482
494
|
try {
|
|
483
495
|
const mode = statSync(dst).mode & 0o777;
|
|
484
496
|
if ((mode & 0o111) === 0) {
|
|
@@ -652,7 +664,7 @@ function coreGuardWorkflowContent() {
|
|
|
652
664
|
' echo "OK: no mixed framework + app changes."\n');
|
|
653
665
|
}
|
|
654
666
|
export function ensureGitattributes(projectDir, io) {
|
|
655
|
-
const path =
|
|
667
|
+
const path = projectionTarget(projectDir, ".gitattributes");
|
|
656
668
|
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
657
669
|
const present = new Set(existing.split("\n").map((line) => line.trim()));
|
|
658
670
|
const additions = CORE_GITATTRIBUTES_LINES.filter((line) => !present.has(line));
|
|
@@ -687,7 +699,7 @@ function appendGreptilePattern(patterns, glob) {
|
|
|
687
699
|
return `${patterns}\n${glob}`;
|
|
688
700
|
}
|
|
689
701
|
export function ensureGreptileIgnore(projectDir, io) {
|
|
690
|
-
const path =
|
|
702
|
+
const path = projectionTarget(projectDir, "greptile.json");
|
|
691
703
|
const fileExisted = existsSync(path);
|
|
692
704
|
let raw = fileExisted ? readFileSync(path, "utf8") : "";
|
|
693
705
|
if (!raw.trim())
|
|
@@ -762,7 +774,7 @@ function insertCodeqlPathsIgnore(content, glob) {
|
|
|
762
774
|
return { content, ok: false };
|
|
763
775
|
}
|
|
764
776
|
export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
765
|
-
const path =
|
|
777
|
+
const path = projectionTarget(projectDir, CODEQL_CONFIG_REL);
|
|
766
778
|
if (!existsSync(path)) {
|
|
767
779
|
mkdirSync(dirname(path), { recursive: true });
|
|
768
780
|
writeFileSync(path, codeqlConfigDefault(), "utf8");
|
|
@@ -783,7 +795,7 @@ export function ensureCodeqlPathsIgnore(projectDir, io) {
|
|
|
783
795
|
return true;
|
|
784
796
|
}
|
|
785
797
|
export function ensureCoreGuardWorkflow(projectDir, io) {
|
|
786
|
-
const path =
|
|
798
|
+
const path = projectionTarget(projectDir, CORE_GUARD_WORKFLOW_REL);
|
|
787
799
|
const desired = coreGuardWorkflowContent();
|
|
788
800
|
if (existsSync(path)) {
|
|
789
801
|
const existing = readFileSync(path, "utf8");
|
|
@@ -862,6 +874,9 @@ export async function depositNeutralization(projectDir, io, options = {}) {
|
|
|
862
874
|
await step();
|
|
863
875
|
}
|
|
864
876
|
catch (cause) {
|
|
877
|
+
if (cause instanceof ProjectionContainmentError) {
|
|
878
|
+
throw cause;
|
|
879
|
+
}
|
|
865
880
|
io.printf(`Warning: neutralization step failed: ${String(cause)}\n`);
|
|
866
881
|
}
|
|
867
882
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
5
5
|
import { call } from "../scm/call.js";
|
|
6
6
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
@@ -217,7 +217,7 @@ export function expandPatterns(patterns, root = null) {
|
|
|
217
217
|
const seen = new Set();
|
|
218
218
|
const out = [];
|
|
219
219
|
for (const pattern of patterns) {
|
|
220
|
-
const candidate = root !== null && !pattern
|
|
220
|
+
const candidate = root !== null && !isAbsolute(pattern) ? join(root, pattern) : pattern;
|
|
221
221
|
let matches = globSync(candidate).sort();
|
|
222
222
|
if (matches.length === 0) {
|
|
223
223
|
try {
|
|
@@ -8,7 +8,7 @@ import { resolveProjectRoot } from "../scope/project-context.js";
|
|
|
8
8
|
import { resolveProjectRepo } from "../slice/project-context.js";
|
|
9
9
|
import { slugify, TODAY } from "../vbrief-build/build.js";
|
|
10
10
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
11
|
-
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY,
|
|
11
|
+
import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
|
|
12
12
|
import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
|
|
13
13
|
import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
|
|
14
14
|
export const INGEST_STATUSES = ["proposed", "pending", "active"];
|
|
@@ -78,7 +78,7 @@ function resolveIngestEmissionLayout(vbriefDir) {
|
|
|
78
78
|
return {
|
|
79
79
|
artifactSuffix: LEGACY_ARTIFACT_SUFFIX,
|
|
80
80
|
infoRootKey: LEGACY_INFO_ROOT_KEY,
|
|
81
|
-
infoVersion:
|
|
81
|
+
infoVersion: EMITTED_VBRIEF_VERSION,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
return {
|
|
@@ -300,11 +300,25 @@ export function issueCommentThread(issue) {
|
|
|
300
300
|
export function issueCommentsAlreadyFetched(issue) {
|
|
301
301
|
return Object.hasOwn(issue, ISSUE_COMMENT_THREAD_KEY);
|
|
302
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Run quarantine `scan()` on untrusted ingest text (#2447). Fail closed on a
|
|
305
|
+
* credential hard-fail; return the fenced/quarantined transform for soft signals.
|
|
306
|
+
*/
|
|
307
|
+
function scanUntrustedIngestText(issueNumber, text) {
|
|
308
|
+
const scanResult = scan(text);
|
|
309
|
+
const hardFails = scanResult.flags.filter((f) => f.severity === "hard-fail");
|
|
310
|
+
if (hardFails.length > 0) {
|
|
311
|
+
throw new ScannerHardFailError(issueNumber, scanResult.flags);
|
|
312
|
+
}
|
|
313
|
+
return scanResult.transformed_content;
|
|
314
|
+
}
|
|
303
315
|
export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
|
|
304
316
|
const number = Number(issue.number);
|
|
305
|
-
const
|
|
317
|
+
const titleRaw = (typeof issue.title === "string" && issue.title.length > 0
|
|
306
318
|
? issue.title
|
|
307
319
|
: `Issue #${number}`) || `Issue #${number}`;
|
|
320
|
+
// #2447: quarantine-scan issue title before it lands in agent-authoritative fields.
|
|
321
|
+
const title = scanUntrustedIngestText(number, titleRaw);
|
|
308
322
|
const url = (typeof issue.url === "string" && issue.url.length > 0 ? issue.url : "") ||
|
|
309
323
|
(repoUrl.length > 0 ? `${repoUrl}/issues/${number}` : "");
|
|
310
324
|
const bodyRaw = issue.body;
|
|
@@ -345,7 +359,12 @@ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
|
|
|
345
359
|
if (labelNames.length > 0) {
|
|
346
360
|
narratives.Labels = labelNames.join(", ");
|
|
347
361
|
}
|
|
348
|
-
const
|
|
362
|
+
const planItemsRaw = bodyStr.length > 0 ? extractPlanItems(bodyStr) : [];
|
|
363
|
+
// #2447: scan each derived plan-item title (empty-body issues still scan title above).
|
|
364
|
+
const planItems = planItemsRaw.map((item) => ({
|
|
365
|
+
...item,
|
|
366
|
+
title: scanUntrustedIngestText(number, String(item.title ?? "")),
|
|
367
|
+
}));
|
|
349
368
|
const plan = {
|
|
350
369
|
title,
|
|
351
370
|
status: planStatus,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type OpenPackageTierName = "daily-core" | "standard" | "advanced";
|
|
2
|
+
export interface OpenPackageTierManifest {
|
|
3
|
+
readonly defaultInstallTier: OpenPackageTierName;
|
|
4
|
+
readonly tiers: Record<OpenPackageTierName, {
|
|
5
|
+
skills: string[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
/** Default OpenPackage install tier from deft-tiers.json (#2494). */
|
|
9
|
+
export declare function getOpenPackageDefaultInstallTier(repoRoot: string): OpenPackageTierName;
|
|
10
|
+
/** Skill names for an OpenPackage tier selection (`all` = every mapped skill). */
|
|
11
|
+
export declare function resolveOpenPackageTierSkills(repoRoot: string, tier: OpenPackageTierName | "all"): string[];
|
|
12
|
+
//# sourceMappingURL=openpackage-tiers.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const TIER_NAMES = ["daily-core", "standard", "advanced"];
|
|
4
|
+
function isOpenPackageTierName(value) {
|
|
5
|
+
return TIER_NAMES.includes(value);
|
|
6
|
+
}
|
|
7
|
+
function loadOpenPackageTierManifest(repoRoot) {
|
|
8
|
+
const path = join(repoRoot, "packaging", "openpackage", "deft-tiers.json");
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
if (parsed === null || typeof parsed !== "object" || !parsed.tiers) {
|
|
11
|
+
throw new Error("deft-tiers.json must be an object with a tiers field");
|
|
12
|
+
}
|
|
13
|
+
for (const tier of TIER_NAMES) {
|
|
14
|
+
const block = parsed.tiers[tier];
|
|
15
|
+
if (!block || !Array.isArray(block.skills)) {
|
|
16
|
+
throw new Error(`deft-tiers.json missing tiers.${tier}.skills`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const defaultInstallTier = parsed.defaultInstallTier ?? "daily-core";
|
|
20
|
+
if (!isOpenPackageTierName(defaultInstallTier)) {
|
|
21
|
+
throw new Error(`deft-tiers.json defaultInstallTier must be one of ${TIER_NAMES.join(", ")}; got ${defaultInstallTier}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
defaultInstallTier,
|
|
25
|
+
tiers: parsed.tiers,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveTierSkills(manifest, tier) {
|
|
29
|
+
if (tier === "all") {
|
|
30
|
+
return [...new Set(TIER_NAMES.flatMap((name) => manifest.tiers[name].skills))];
|
|
31
|
+
}
|
|
32
|
+
return [...manifest.tiers[tier].skills];
|
|
33
|
+
}
|
|
34
|
+
/** Default OpenPackage install tier from deft-tiers.json (#2494). */
|
|
35
|
+
export function getOpenPackageDefaultInstallTier(repoRoot) {
|
|
36
|
+
return loadOpenPackageTierManifest(repoRoot).defaultInstallTier;
|
|
37
|
+
}
|
|
38
|
+
/** Skill names for an OpenPackage tier selection (`all` = every mapped skill). */
|
|
39
|
+
export function resolveOpenPackageTierSkills(repoRoot, tier) {
|
|
40
|
+
return resolveTierSkills(loadOpenPackageTierManifest(repoRoot), tier);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=openpackage-tiers.js.map
|
|
@@ -9,9 +9,32 @@
|
|
|
9
9
|
* it is an explicit, reviewed diff to this typed field -- that diff IS the
|
|
10
10
|
* "was this growth deliberate?" checkpoint.
|
|
11
11
|
*/
|
|
12
|
+
export type HarnessProfile = "cursor" | "none";
|
|
13
|
+
export type SkillFrontmatterTier = "daily-core" | "all" | "none";
|
|
12
14
|
export interface AgentsMdBudget {
|
|
13
15
|
readonly managedMaxLines: number;
|
|
14
16
|
readonly unmanagedMaxLines: number;
|
|
17
|
+
/**
|
|
18
|
+
* Fail-closed UTF-8 byte ratchet for the managed section (#2452).
|
|
19
|
+
* Seeded at the current measured size so the gate ships green; growth past
|
|
20
|
+
* this ceiling fails. The <=8192 B north-star is reported separately.
|
|
21
|
+
*/
|
|
22
|
+
readonly absoluteMaxBytes?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Harness profile for DD-3 skill-frontmatter measurement (#2463).
|
|
25
|
+
* When unset, the gate auto-detects `cursor` when `content/skills/` exists.
|
|
26
|
+
*/
|
|
27
|
+
readonly harnessProfile?: HarnessProfile;
|
|
28
|
+
/**
|
|
29
|
+
* Skill tier for DD-3 frontmatter caps (`daily-core` vs `all` vs `none`).
|
|
30
|
+
* Defaults to `all` for reporting when unset.
|
|
31
|
+
*/
|
|
32
|
+
readonly skillFrontmatterTier?: SkillFrontmatterTier;
|
|
33
|
+
/**
|
|
34
|
+
* Optional fail-closed ratchet for DD-3 skill-frontmatter bytes (#2463).
|
|
35
|
+
* Advisory when unset; growth past this ceiling fails when set.
|
|
36
|
+
*/
|
|
37
|
+
readonly skillFrontmatterMaxBytes?: number;
|
|
15
38
|
}
|
|
16
39
|
export type AgentsMdBudgetSource = "typed" | "unset" | "default-on-error";
|
|
17
40
|
export interface AgentsMdBudgetResult {
|
|
@@ -39,6 +39,19 @@ function readRegion(block, key) {
|
|
|
39
39
|
}
|
|
40
40
|
return { value: raw, error: null };
|
|
41
41
|
}
|
|
42
|
+
function readOptionalRegion(block, key) {
|
|
43
|
+
if (!(key in block)) {
|
|
44
|
+
return { value: undefined, error: null };
|
|
45
|
+
}
|
|
46
|
+
const raw = block[key];
|
|
47
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
48
|
+
return {
|
|
49
|
+
value: undefined,
|
|
50
|
+
error: `plan.policy.agentsMdBudget.${key} must be a non-negative integer; got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { value: raw, error: null };
|
|
54
|
+
}
|
|
42
55
|
/** Resolve plan.policy.agentsMdBudget from PROJECT-DEFINITION (#645). */
|
|
43
56
|
export function resolveAgentsMdBudget(projectRoot) {
|
|
44
57
|
const [data, err] = loadProjectDefinition(projectRoot);
|
|
@@ -77,13 +90,71 @@ export function resolveAgentsMdBudget(projectRoot) {
|
|
|
77
90
|
if (unmanaged.error !== null) {
|
|
78
91
|
return { budget: null, source: "default-on-error", error: unmanaged.error };
|
|
79
92
|
}
|
|
93
|
+
const absolute = readOptionalRegion(block, "absoluteMaxBytes");
|
|
94
|
+
if (absolute.error !== null) {
|
|
95
|
+
return { budget: null, source: "default-on-error", error: absolute.error };
|
|
96
|
+
}
|
|
97
|
+
const skillFrontmatterMax = readOptionalRegion(block, "skillFrontmatterMaxBytes");
|
|
98
|
+
if (skillFrontmatterMax.error !== null) {
|
|
99
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterMax.error };
|
|
100
|
+
}
|
|
101
|
+
const harnessProfile = readOptionalHarnessProfile(block, "harnessProfile");
|
|
102
|
+
if (harnessProfile.error !== null) {
|
|
103
|
+
return { budget: null, source: "default-on-error", error: harnessProfile.error };
|
|
104
|
+
}
|
|
105
|
+
const skillFrontmatterTier = readOptionalSkillTier(block, "skillFrontmatterTier");
|
|
106
|
+
if (skillFrontmatterTier.error !== null) {
|
|
107
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterTier.error };
|
|
108
|
+
}
|
|
109
|
+
const budget = {
|
|
110
|
+
managedMaxLines: managed.value,
|
|
111
|
+
unmanagedMaxLines: unmanaged.value,
|
|
112
|
+
};
|
|
113
|
+
const withOptional = {
|
|
114
|
+
...budget,
|
|
115
|
+
...(absolute.value !== undefined ? { absoluteMaxBytes: absolute.value } : {}),
|
|
116
|
+
...(skillFrontmatterMax.value !== undefined
|
|
117
|
+
? { skillFrontmatterMaxBytes: skillFrontmatterMax.value }
|
|
118
|
+
: {}),
|
|
119
|
+
...(harnessProfile.value !== undefined ? { harnessProfile: harnessProfile.value } : {}),
|
|
120
|
+
...(skillFrontmatterTier.value !== undefined
|
|
121
|
+
? { skillFrontmatterTier: skillFrontmatterTier.value }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
80
124
|
return {
|
|
81
|
-
budget:
|
|
82
|
-
managedMaxLines: managed.value,
|
|
83
|
-
unmanagedMaxLines: unmanaged.value,
|
|
84
|
-
},
|
|
125
|
+
budget: withOptional,
|
|
85
126
|
source: "typed",
|
|
86
127
|
error: null,
|
|
87
128
|
};
|
|
88
129
|
}
|
|
130
|
+
const HARNESS_PROFILES = new Set(["cursor", "none"]);
|
|
131
|
+
const SKILL_FRONTMATTER_TIERS = new Set(["daily-core", "all", "none"]);
|
|
132
|
+
function readOptionalHarnessProfile(block, key) {
|
|
133
|
+
if (!(key in block)) {
|
|
134
|
+
return { value: undefined, error: null };
|
|
135
|
+
}
|
|
136
|
+
const raw = block[key];
|
|
137
|
+
if (typeof raw !== "string" || !HARNESS_PROFILES.has(raw)) {
|
|
138
|
+
return {
|
|
139
|
+
value: undefined,
|
|
140
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...HARNESS_PROFILES].join(", ")}; ` +
|
|
141
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { value: raw, error: null };
|
|
145
|
+
}
|
|
146
|
+
function readOptionalSkillTier(block, key) {
|
|
147
|
+
if (!(key in block)) {
|
|
148
|
+
return { value: undefined, error: null };
|
|
149
|
+
}
|
|
150
|
+
const raw = block[key];
|
|
151
|
+
if (typeof raw !== "string" || !SKILL_FRONTMATTER_TIERS.has(raw)) {
|
|
152
|
+
return {
|
|
153
|
+
value: undefined,
|
|
154
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...SKILL_FRONTMATTER_TIERS].join(", ")}; ` +
|
|
155
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { value: raw, error: null };
|
|
159
|
+
}
|
|
89
160
|
//# sourceMappingURL=agents-md-budget.js.map
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
export declare const ACTIVE_FOLDER = "active";
|
|
3
3
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
4
4
|
export declare const ELIGIBLE_STATUS = "running";
|
|
5
|
-
/** Actionable redirect appended to every reject path (#810). */
|
|
6
|
-
export declare const ACTIVATE_HINT = "Run `task vbrief:activate {path}` before spawning an implementation agent.";
|
|
5
|
+
/** Actionable redirect appended to every reject path (#810 / #2449). */
|
|
6
|
+
export declare const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
|
|
7
|
+
/** Lifecycle folder names eligible for implementation (#810). */
|
|
8
|
+
export declare const ELIGIBLE_LIFECYCLE_DIRS: readonly ["xbrief/active", "vbrief/active"];
|
|
9
|
+
export declare const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
|
|
7
10
|
/** Result of a vBRIEF preflight evaluation; mirrors the Python `evaluate` tuple. */
|
|
8
11
|
export interface EvaluateResult {
|
|
9
12
|
readonly exitCode: 0 | 1;
|
|
@@ -4,14 +4,17 @@ import { basename, dirname } from "node:path";
|
|
|
4
4
|
export const ACTIVE_FOLDER = "active";
|
|
5
5
|
/** Canonical eligibility status — only `running` signals an active handoff. */
|
|
6
6
|
export const ELIGIBLE_STATUS = "running";
|
|
7
|
-
/** Actionable redirect appended to every reject path (#810). */
|
|
8
|
-
export const ACTIVATE_HINT = "Run `task vbrief:activate {path}` before spawning an implementation agent.";
|
|
7
|
+
/** Actionable redirect appended to every reject path (#810 / #2449). */
|
|
8
|
+
export const ACTIVATE_HINT = "Run `task scope:activate -- {path}` (or legacy `task vbrief:activate -- {path}`) before spawning an implementation agent.";
|
|
9
|
+
/** Lifecycle folder names eligible for implementation (#810). */
|
|
10
|
+
export const ELIGIBLE_LIFECYCLE_DIRS = ["xbrief/active", "vbrief/active"];
|
|
11
|
+
export const PREFLIGHT_USAGE_HINT = "Expected: `task xbrief:preflight -- xbrief/active/<story>.xbrief.json` (legacy: `task vbrief:preflight -- <path>`).";
|
|
9
12
|
/** Substitute `{path}` without `$`-pattern expansion in user paths (#1721). */
|
|
10
13
|
export function formatActivateHint(path) {
|
|
11
14
|
return ACTIVATE_HINT.replace("{path}", () => path);
|
|
12
15
|
}
|
|
13
16
|
function buildReject(path, reason) {
|
|
14
|
-
return `${reason}\n ${formatActivateHint(path)}`;
|
|
17
|
+
return `${reason}\n ${PREFLIGHT_USAGE_HINT}\n ${formatActivateHint(path)}`;
|
|
15
18
|
}
|
|
16
19
|
/** Map Node `JSON.parse` errors to CPython `json.JSONDecodeError.msg` for parity (#1721). */
|
|
17
20
|
function nodeJsonErrorToPythonMsg(nodeMessage) {
|
|
@@ -91,10 +94,12 @@ export function evaluate(vbriefPath) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
const folder = basename(dirname(vbriefPath));
|
|
97
|
+
const parent = basename(dirname(dirname(vbriefPath)));
|
|
98
|
+
const lifecycleDir = `${parent}/${folder}`;
|
|
94
99
|
if (folder !== ACTIVE_FOLDER) {
|
|
95
100
|
return {
|
|
96
101
|
exitCode: 1,
|
|
97
|
-
message: buildReject(path, `
|
|
102
|
+
message: buildReject(path, `xBRIEF is in ${lifecycleDir}/ -- only xbrief/active/ (or legacy vbrief/active/) is eligible for implementation.`),
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
105
|
const record = payload;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { EvaluateResult } from "./evaluate.js";
|
|
2
|
-
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, } from "./evaluate.js";
|
|
2
|
+
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/preflight/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, } from "./evaluate.js";
|
|
1
|
+
export { ACTIVATE_HINT, ACTIVE_FOLDER, ELIGIBLE_LIFECYCLE_DIRS, ELIGIBLE_STATUS, emitJson, evaluate, formatActivateHint, PREFLIGHT_USAGE_HINT, } from "./evaluate.js";
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/** Seed a real on-disk project root for pipeline write-path tests (#2470). */
|
|
5
|
+
export function seedReleaseProjectDir(changelog = `## [Unreleased]\n\n### Added\n- x\n`) {
|
|
6
|
+
const dir = mkdtempSync(join(tmpdir(), "release-proj-"));
|
|
7
|
+
writeFileSync(join(dir, "CHANGELOG.md"), changelog, "utf8");
|
|
8
|
+
writeFileSync(join(dir, "ROADMAP.md"), "# Roadmap\n", "utf8");
|
|
9
|
+
return dir;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=pipeline-fixture.js.map
|
package/dist/release/pipeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { assertProjectionContained } from "../fs/projection-containment.js";
|
|
3
4
|
import { prependUpgradeBanner, promoteChangelog, sectionForVersion } from "./changelog.js";
|
|
4
5
|
import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
|
|
5
6
|
import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
|
|
@@ -16,6 +17,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
16
17
|
const version = config.version;
|
|
17
18
|
const today = (seams.todayIso ?? todayIso)();
|
|
18
19
|
const changelogPath = join(projectRoot, "CHANGELOG.md");
|
|
20
|
+
const roadmapPath = join(projectRoot, "ROADMAP.md");
|
|
19
21
|
const readFile = seams.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
20
22
|
const writeFile = seams.writeFile ?? ((p, c) => writeFileSync(p, c, "utf8"));
|
|
21
23
|
const fileExists = seams.fileExists ?? ((p) => existsSync(p));
|
|
@@ -149,6 +151,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
149
151
|
emit(6, label, `DRYRUN (would rewrite CHANGELOG.md: ## [Unreleased] -> ## [${version}] - ${today}; new compare link added;${summaryNote})`);
|
|
150
152
|
}
|
|
151
153
|
else {
|
|
154
|
+
assertProjectionContained(projectRoot, changelogPath);
|
|
152
155
|
writeFile(changelogPath, promotedChangelog);
|
|
153
156
|
emit(6, label, `OK (## [${version}] - ${today};${summaryNote})`);
|
|
154
157
|
}
|
|
@@ -158,6 +161,7 @@ export function runPipeline(config, seams = {}) {
|
|
|
158
161
|
emit(7, label, "DRYRUN (would run task roadmap:render)");
|
|
159
162
|
}
|
|
160
163
|
else {
|
|
164
|
+
assertProjectionContained(projectRoot, roadmapPath);
|
|
161
165
|
const [ok, reason] = refreshRoadmapFn(projectRoot);
|
|
162
166
|
if (ok) {
|
|
163
167
|
emit(7, label, `OK (${reason})`);
|
package/dist/release/spawn.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
3
3
|
export function defaultWhich(name) {
|
|
4
|
-
const
|
|
4
|
+
const locator = process.platform === "win32" ? "where" : "which";
|
|
5
|
+
const result = spawnSync(locator, [name], { encoding: "utf8" });
|
|
5
6
|
if (result.status !== 0) {
|
|
6
7
|
return null;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// `where` on Windows may return multiple lines; take the first non-empty match.
|
|
10
|
+
const first = (result.stdout ?? "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
11
|
+
return first ? first.trim() : null;
|
|
10
12
|
}
|
|
11
13
|
export function spawnText(cmd, args, options = {}) {
|
|
12
14
|
const result = spawnSync(cmd, [...args], {
|
package/dist/scope/decompose.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that draft, writes the child story vBRIEFs, and updates the parent scope references.
|
|
8
8
|
*/
|
|
9
9
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
11
11
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
12
12
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
13
13
|
import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
|
|
@@ -415,7 +415,7 @@ function vbriefDir(projectRoot) {
|
|
|
415
415
|
function relToVbrief(vbriefDirPath, path) {
|
|
416
416
|
const resolvedPath = resolve(path);
|
|
417
417
|
const resolvedVbrief = resolve(vbriefDirPath);
|
|
418
|
-
if (resolvedPath.startsWith(
|
|
418
|
+
if (resolvedPath.startsWith(resolvedVbrief + sep) || resolvedPath === resolvedVbrief) {
|
|
419
419
|
return resolvedPath.slice(resolvedVbrief.length + 1).replace(/\\/g, "/");
|
|
420
420
|
}
|
|
421
421
|
throw new DecompositionError(`${path}: path must be inside ${vbriefDirPath}`);
|
|
@@ -823,7 +823,8 @@ export function applyDecomposition(opts) {
|
|
|
823
823
|
if (!LIFECYCLE_FOLDERS.has(outputFolderName)) {
|
|
824
824
|
throw new DecompositionError("output_dir must be a vbrief lifecycle folder");
|
|
825
825
|
}
|
|
826
|
-
|
|
826
|
+
const resolvedVbriefDir = resolve(vbriefDirPath);
|
|
827
|
+
if (!outputDir.startsWith(resolvedVbriefDir + sep) && outputDir !== resolvedVbriefDir) {
|
|
827
828
|
throw new DecompositionError("output_dir must be inside vbrief/");
|
|
828
829
|
}
|
|
829
830
|
if (outputFolderName === "active") {
|
package/dist/scope/transition.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
|
|
4
|
+
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
5
6
|
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
6
7
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
@@ -76,11 +77,25 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
76
77
|
message: `No-op: ${basename} is already in ${currentFolder}/ (status: ${currentStatus})`,
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
const vbriefRoot = dirname(dirname(resolvedPath));
|
|
81
|
+
const projectRoot = dirname(vbriefRoot);
|
|
82
|
+
if (targetFolder !== null) {
|
|
83
|
+
const destDir = join(vbriefRoot, targetFolder);
|
|
84
|
+
try {
|
|
85
|
+
// #2447: refuse lifecycle moves when the destination folder escapes the checkout.
|
|
86
|
+
// Run before mutating the source file so a refusal leaves lifecycle state intact.
|
|
87
|
+
assertProjectionContained(projectRoot, destDir);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof ProjectionContainmentError) {
|
|
91
|
+
return { ok: false, message: err.message };
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
79
96
|
const nowIso = utcNowIso(now);
|
|
80
97
|
planObj.status = targetStatus;
|
|
81
98
|
planObj.updated = nowIso;
|
|
82
|
-
const vbriefRoot = dirname(dirname(resolvedPath));
|
|
83
|
-
const projectRoot = dirname(vbriefRoot);
|
|
84
99
|
if (act === "complete") {
|
|
85
100
|
stampCompletionMetadata(planObj, projectRoot, nowIso);
|
|
86
101
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
1
2
|
import { type ResolveLifecycleArtifactRefOptions } from "../layout/lifecycle-ref.js";
|
|
3
|
+
export { ProjectionContainmentError as LifecycleRefContainmentError };
|
|
4
|
+
/** Reject absolute or `..`-escaping lifecycle refs before path resolution (#2470). */
|
|
5
|
+
export declare function rejectEscapingLifecycleRel(rel: string): void;
|
|
6
|
+
/** Assert a resolved lifecycle artifact path stays under the lifecycle root (#2470). */
|
|
7
|
+
export declare function assertLifecycleArtifactContained(lifecycleRoot: string, resolvedPath: string): void;
|
|
2
8
|
/** Resolve a vBRIEF reference URI to an absolute path, or null. */
|
|
3
9
|
export declare function resolveVbriefRef(uri: unknown, vbriefDir: string, options?: ResolveLifecycleArtifactRefOptions): string | null;
|
|
4
10
|
/** Collect planRef values from the plan root and top-level items. */
|