@deftai/directive-core 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/content-contracts/skills/helpers.d.ts +6 -0
  2. package/dist/content-contracts/skills/helpers.js +47 -3
  3. package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
  4. package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
  5. package/dist/doctor/checks.d.ts +6 -0
  6. package/dist/doctor/checks.js +139 -0
  7. package/dist/doctor/main.js +2 -1
  8. package/dist/eval/crud-telemetry.d.ts +2 -0
  9. package/dist/eval/crud-telemetry.js +30 -0
  10. package/dist/intake/github-body-cli.js +2 -0
  11. package/dist/intake/github-body.d.ts +11 -0
  12. package/dist/intake/github-body.js +95 -24
  13. package/dist/integration-e2e/helpers.js +1 -1
  14. package/dist/issue-sync/sync-from-xbrief.js +3 -0
  15. package/dist/orphan-active/evaluate.d.ts +25 -0
  16. package/dist/orphan-active/evaluate.js +275 -0
  17. package/dist/orphan-active/index.d.ts +3 -0
  18. package/dist/orphan-active/index.js +3 -0
  19. package/dist/orphan-active/refs.d.ts +16 -0
  20. package/dist/orphan-active/refs.js +105 -0
  21. package/dist/policy/index.d.ts +1 -0
  22. package/dist/policy/index.js +19 -5
  23. package/dist/policy/product-signal.d.ts +45 -0
  24. package/dist/policy/product-signal.js +210 -0
  25. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  26. package/dist/pr-wait-mergeable/cascade.js +18 -0
  27. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  28. package/dist/pr-wait-mergeable/main.js +44 -73
  29. package/dist/pr-wait-mergeable/result.d.ts +2 -1
  30. package/dist/pr-wait-mergeable/result.js +4 -0
  31. package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
  32. package/dist/pr-wait-mergeable/semantic-green.js +202 -0
  33. package/dist/pr-wait-mergeable/types.d.ts +1 -0
  34. package/dist/pr-watch/constants.d.ts +4 -4
  35. package/dist/pr-watch/constants.js +4 -4
  36. package/dist/pr-watch/watch.js +5 -3
  37. package/dist/product-signal/actor-name.d.ts +18 -0
  38. package/dist/product-signal/actor-name.js +94 -0
  39. package/dist/product-signal/consent.d.ts +30 -0
  40. package/dist/product-signal/consent.js +116 -0
  41. package/dist/product-signal/gates.d.ts +17 -0
  42. package/dist/product-signal/gates.js +66 -0
  43. package/dist/product-signal/github-private-sink-adapter.d.ts +28 -0
  44. package/dist/product-signal/github-private-sink-adapter.js +274 -0
  45. package/dist/product-signal/headless.d.ts +8 -0
  46. package/dist/product-signal/headless.js +32 -0
  47. package/dist/product-signal/install-context.d.ts +13 -0
  48. package/dist/product-signal/install-context.js +41 -0
  49. package/dist/product-signal/local-signal-summary.d.ts +7 -0
  50. package/dist/product-signal/local-signal-summary.js +173 -0
  51. package/dist/product-signal/payload.d.ts +90 -0
  52. package/dist/product-signal/payload.js +124 -0
  53. package/dist/product-signal/sink-bootstrap.d.ts +29 -0
  54. package/dist/product-signal/sink-bootstrap.js +108 -0
  55. package/dist/product-signal/submit-adapter.d.ts +14 -0
  56. package/dist/product-signal/submit-adapter.js +2 -0
  57. package/dist/product-signal/submit.d.ts +54 -0
  58. package/dist/product-signal/submit.js +290 -0
  59. package/dist/release/native-steps.js +11 -4
  60. package/dist/render/framework-commands.js +6 -0
  61. package/dist/scm/gh-rest.d.ts +3 -1
  62. package/dist/scm/gh-rest.js +22 -0
  63. package/dist/scope/brief-io.d.ts +26 -0
  64. package/dist/scope/brief-io.js +77 -0
  65. package/dist/scope/decompose.js +2 -2
  66. package/dist/scope/decomposed-refs.js +3 -3
  67. package/dist/scope/demote.js +2 -2
  68. package/dist/scope/project-definition-sync.d.ts +2 -2
  69. package/dist/scope/project-definition-sync.js +15 -3
  70. package/dist/scope/registry-artifact-sync.d.ts +6 -1
  71. package/dist/scope/registry-artifact-sync.js +31 -13
  72. package/dist/scope/transition.js +23 -15
  73. package/dist/scope/undo.js +2 -2
  74. package/dist/scope/vbrief-json.d.ts +1 -2
  75. package/dist/scope/vbrief-json.js +1 -4
  76. package/dist/staleness-tickler/state.js +19 -2
  77. package/dist/triage/queue/cache.js +5 -0
  78. package/dist/verify-source/index.d.ts +1 -0
  79. package/dist/verify-source/index.js +1 -0
  80. package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
  81. package/dist/verify-source/skill-external-fetch-gate.js +71 -0
  82. package/package.json +7 -3
@@ -82,6 +82,9 @@ export const COMMANDS = {
82
82
  "verify:wip-cap": spec("verify:wip-cap", "preflight_wip_cap:main", {
83
83
  projectRootArg: "--project-root",
84
84
  }),
85
+ "verify:orphan-active": spec("verify:orphan-active", "verify_orphan_active:main", {
86
+ projectRootArg: "--project-root",
87
+ }),
85
88
  "verify:pack-drift": spec("verify:pack-drift", "pack_render:main", {
86
89
  defaultArgs: ["--check"],
87
90
  cwd: "framework",
@@ -102,6 +105,7 @@ export const COMMANDS = {
102
105
  "verify:branch",
103
106
  "verify:cache-fresh",
104
107
  "verify:wip-cap",
108
+ "verify:orphan-active",
105
109
  "vbrief:validate",
106
110
  "verify-strategy-output",
107
111
  ]),
@@ -120,6 +124,7 @@ export const COMMANDS = {
120
124
  "verify:cache-fresh",
121
125
  "verify:pack-drift",
122
126
  "verify:wip-cap",
127
+ "verify:orphan-active",
123
128
  "vbrief:validate",
124
129
  "verify-strategy-output",
125
130
  ]),
@@ -293,6 +298,7 @@ const ENTRYPOINT_VERB = {
293
298
  "verify_no_task_runtime:main": "verify-no-task-runtime",
294
299
  "preflight_cache:main": "preflight-cache",
295
300
  "preflight_wip_cap:main": "verify-wip-cap",
301
+ "verify_orphan_active:main": "verify-orphan-active",
296
302
  "pack_render:main": "pack-render",
297
303
  "validate_strategy_output:main": "validate-strategy-output",
298
304
  "vbrief_validate:main": "vbrief-validate",
@@ -53,9 +53,11 @@ export interface RestIssueListOptions {
53
53
  export declare function restIssueList(repo: string, options?: RestIssueListOptions, seams?: GhRestSeams): Record<string, unknown>[];
54
54
  export declare const REST_MAX_PER_PAGE = 100;
55
55
  export declare const REST_PAGINATION_MAX_PAGES = 100;
56
- export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
56
+ export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
57
57
  export declare function restCreateIssue(repo: string, title: string, body: string, labels?: readonly string[], seams?: GhRestSeams): Record<string, unknown>;
58
58
  export declare function restPostComment(repo: string, n: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
59
+ export declare function restUpdateIssue(repo: string, n: number, patch: Record<string, unknown>, seams?: GhRestSeams): Record<string, unknown>;
60
+ export declare function restCreateLabel(repo: string, name: string, color: string, description: string, seams?: GhRestSeams): Record<string, unknown>;
59
61
  export declare function restCloseIssue(repo: string, n: number, reason?: string | null, seams?: GhRestSeams): Record<string, unknown>;
60
62
  export declare function restOpenPr(repo: string, head: string, base: string, title: string, body: string, options?: {
61
63
  draft?: boolean;
@@ -171,6 +171,8 @@ export const REST_PAGINATION_MAX_PAGES = 100;
171
171
  export const PUBLIC_HELPERS = [
172
172
  "restCreateIssue",
173
173
  "restPostComment",
174
+ "restUpdateIssue",
175
+ "restCreateLabel",
174
176
  "restCloseIssue",
175
177
  "restOpenPr",
176
178
  "restMergePr",
@@ -224,6 +226,26 @@ export function restPostComment(repo, n, body, seams = {}) {
224
226
  ...seams,
225
227
  });
226
228
  }
229
+ export function restUpdateIssue(repo, n, patch, seams = {}) {
230
+ const [owner, name] = splitRepo(repo);
231
+ const endpoint = `repos/${owner}/${name}/issues/${n}`;
232
+ return execMutation([endpoint, "--method", "PATCH"], {
233
+ endpoint,
234
+ payload: patch,
235
+ hint: "verify repo permissions and issue number; check gh auth status",
236
+ ...seams,
237
+ });
238
+ }
239
+ export function restCreateLabel(repo, name, color, description, seams = {}) {
240
+ const [owner, repoName] = splitRepo(repo);
241
+ const endpoint = `repos/${owner}/${repoName}/labels`;
242
+ return execMutation([endpoint, "--method", "POST"], {
243
+ endpoint,
244
+ payload: { name, color, description },
245
+ hint: "verify repo permissions; label may already exist (422 is acceptable for idempotent bootstrap)",
246
+ ...seams,
247
+ });
248
+ }
227
249
  export function restCloseIssue(repo, n, reason = "completed", seams = {}) {
228
250
  const [owner, name] = splitRepo(repo);
229
251
  const endpoint = `repos/${owner}/${name}/issues/${n}`;
@@ -0,0 +1,26 @@
1
+ import type { JsonObject } from "../vbrief-build/types.js";
2
+ export declare class BriefIOError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** Canonical brief serialization (single source of truth with vbrief-build). */
6
+ export declare function formatBriefJson(data: unknown): string;
7
+ export type ReadBriefResult = {
8
+ readonly ok: true;
9
+ readonly data: JsonObject;
10
+ } | {
11
+ readonly ok: false;
12
+ readonly message: string;
13
+ };
14
+ /** Read a scope brief for in-memory mutation (parse only; no validation gate on read). */
15
+ export declare function readBriefForMutation(filePath: string): ReadBriefResult;
16
+ /** Fail-closed schema + folder/status validation before persist. */
17
+ export declare function validateBriefForPersist(filePath: string, data: JsonObject, vbriefRoot: string): string | null;
18
+ export type AtomicWriteBriefResult = {
19
+ readonly ok: true;
20
+ } | {
21
+ readonly ok: false;
22
+ readonly message: string;
23
+ };
24
+ /** Validate then atomically persist a brief (temp file + rename). */
25
+ export declare function atomicWriteBrief(filePath: string, data: JsonObject, vbriefRoot: string): AtomicWriteBriefResult;
26
+ //# sourceMappingURL=brief-io.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { atomicWriteText } from "../cache/io.js";
4
+ import { hasArtifactSuffix } from "../layout/resolve.js";
5
+ import { pythonJsonPretty } from "../vbrief-build/json.js";
6
+ import { validateFolderStatus } from "../vbrief-validate/folder-status.js";
7
+ import { validateVbriefSchema } from "../vbrief-validate/schema.js";
8
+ export class BriefIOError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "BriefIOError";
12
+ }
13
+ }
14
+ /** Canonical brief serialization (single source of truth with vbrief-build). */
15
+ export function formatBriefJson(data) {
16
+ return pythonJsonPretty(data);
17
+ }
18
+ /** Read a scope brief for in-memory mutation (parse only; no validation gate on read). */
19
+ export function readBriefForMutation(filePath) {
20
+ const resolvedPath = resolve(filePath);
21
+ if (!existsSync(resolvedPath)) {
22
+ return { ok: false, message: `File not found: ${resolvedPath}` };
23
+ }
24
+ const basename = resolvedPath.split(/[/\\]/).pop() ?? "";
25
+ if (!hasArtifactSuffix(basename)) {
26
+ return {
27
+ ok: false,
28
+ message: `Not a vBRIEF file (expected .vbrief.json or .xbrief.json): ${basename}`,
29
+ };
30
+ }
31
+ let raw;
32
+ try {
33
+ raw = readFileSync(resolvedPath, "utf8");
34
+ }
35
+ catch (err) {
36
+ const msg = err instanceof Error ? err.message : String(err);
37
+ return { ok: false, message: `Could not read ${resolvedPath}: ${msg}` };
38
+ }
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(raw);
42
+ }
43
+ catch (err) {
44
+ return { ok: false, message: `Invalid JSON in ${resolvedPath}: ${String(err)}` };
45
+ }
46
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
47
+ return { ok: false, message: `Top-level value in ${resolvedPath} is not a JSON object` };
48
+ }
49
+ return { ok: true, data: parsed };
50
+ }
51
+ /** Fail-closed schema + folder/status validation before persist. */
52
+ export function validateBriefForPersist(filePath, data, vbriefRoot) {
53
+ const errors = [
54
+ ...validateVbriefSchema(data, filePath),
55
+ ...validateFolderStatus(filePath, data, vbriefRoot),
56
+ ];
57
+ if (errors.length === 0) {
58
+ return null;
59
+ }
60
+ return errors.join("; ");
61
+ }
62
+ /** Validate then atomically persist a brief (temp file + rename). */
63
+ export function atomicWriteBrief(filePath, data, vbriefRoot) {
64
+ const validationError = validateBriefForPersist(filePath, data, vbriefRoot);
65
+ if (validationError !== null) {
66
+ return { ok: false, message: validationError };
67
+ }
68
+ try {
69
+ atomicWriteText(filePath, formatBriefJson(data));
70
+ return { ok: true };
71
+ }
72
+ catch (err) {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return { ok: false, message: `Failed to write ${filePath}: ${msg}` };
75
+ }
76
+ }
77
+ //# sourceMappingURL=brief-io.js.map
@@ -12,7 +12,7 @@ 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";
14
14
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
15
- import { formatVbriefJson } from "./vbrief-json.js";
15
+ import { formatBriefJson } from "./vbrief-json.js";
16
16
  // ---------------------------------------------------------------------------
17
17
  // Constants
18
18
  // ---------------------------------------------------------------------------
@@ -54,7 +54,7 @@ function loadJson(path) {
54
54
  }
55
55
  function writeJson(path, data) {
56
56
  mkdirSync(dirname(path), { recursive: true });
57
- writeFileSync(path, formatVbriefJson(data), "utf8");
57
+ writeFileSync(path, formatBriefJson(data), "utf8");
58
58
  }
59
59
  // ---------------------------------------------------------------------------
60
60
  // Story quality helpers (ported from _vbrief_story_quality.py)
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, relative, resolve } from "node:path";
3
3
  import { referenceTypeMatches } from "@deftai/directive-types";
4
- import { formatVbriefJson } from "./vbrief-json.js";
4
+ import { formatBriefJson } from "./vbrief-json.js";
5
5
  import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "./vbrief-ref.js";
6
6
  function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
7
7
  if (typeof value !== "string" || value.length === 0) {
@@ -53,7 +53,7 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
53
53
  }
54
54
  if (changed) {
55
55
  try {
56
- writeFileSync(parentPath, formatVbriefJson(parentData), "utf8");
56
+ writeFileSync(parentPath, formatBriefJson(parentData), "utf8");
57
57
  }
58
58
  catch {
59
59
  return false;
@@ -127,7 +127,7 @@ function rewriteChildParentReference(childPath, oldParentResolved, newParentRel,
127
127
  }
128
128
  if (changed) {
129
129
  try {
130
- writeFileSync(childPath, formatVbriefJson(childData), "utf8");
130
+ writeFileSync(childPath, formatBriefJson(childData), "utf8");
131
131
  }
132
132
  catch {
133
133
  return false;
@@ -5,7 +5,7 @@ import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js"
5
5
  import { stripTrailingPathSeparators } from "../text/redos-safe.js";
6
6
  import { append, canonicalLogPath, latestForPath, newDecisionId } from "./audit-log.js";
7
7
  import { resolveProjectRoot } from "./project-context.js";
8
- import { formatVbriefJson, utcNowIso } from "./vbrief-json.js";
8
+ import { formatBriefJson, utcNowIso } from "./vbrief-json.js";
9
9
  import { canonicalRelpath } from "./vbrief-ref.js";
10
10
  export const DEFAULT_OLDER_THAN_DAYS = 45;
11
11
  export const SOURCE_FOLDER = "pending";
@@ -85,7 +85,7 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
85
85
  const timestamp = utcNowIso(now);
86
86
  planObj.status = TARGET_STATUS;
87
87
  planObj.updated = timestamp;
88
- writeFileSync(resolved, formatVbriefJson(data), "utf8");
88
+ writeFileSync(resolved, formatBriefJson(data), "utf8");
89
89
  const vbriefRoot = dirname(dirname(resolved));
90
90
  const targetDir = join(vbriefRoot, TARGET_FOLDER);
91
91
  mkdirSync(targetDir, { recursive: true });
@@ -1,3 +1,3 @@
1
- /** Best-effort sync of PROJECT-DEFINITION after a lifecycle move (#1527). */
2
- export declare function syncProjectDefinitionAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
1
+ /** Fail-closed sync of PROJECT-DEFINITION after a lifecycle move (#1527 / #2131). */
2
+ export declare function syncProjectDefinitionAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): string | null;
3
3
  //# sourceMappingURL=project-definition-sync.d.ts.map
@@ -1,10 +1,22 @@
1
1
  import { dirname, resolve } from "node:path";
2
2
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
3
+ import { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
3
4
  import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
4
- /** Best-effort sync of PROJECT-DEFINITION after a lifecycle move (#1527). */
5
+ /** Fail-closed sync of PROJECT-DEFINITION after a lifecycle move (#1527 / #2131). */
5
6
  export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
6
7
  const projectRoot = dirname(resolve(vbriefRoot));
7
- const projectDefPath = resolveProjectDefinitionPath(projectRoot);
8
- syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus);
8
+ try {
9
+ projectDefinitionMutationLock(projectRoot, () => {
10
+ const projectDefPath = resolveProjectDefinitionPath(projectRoot);
11
+ syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, {
12
+ loadForMutation: () => loadProjectDefinitionForMutation(projectRoot),
13
+ persist: (path, data) => atomicWriteProjectDefinition(path, data),
14
+ });
15
+ });
16
+ return null;
17
+ }
18
+ catch (err) {
19
+ return err instanceof Error ? err.message : String(err);
20
+ }
9
21
  }
10
22
  //# sourceMappingURL=project-definition-sync.js.map
@@ -1,3 +1,8 @@
1
+ import type { JsonObject } from "../vbrief-build/types.js";
2
+ export interface RegistryArtifactPersistHooks {
3
+ readonly loadForMutation?: () => [JsonObject, string];
4
+ readonly persist?: (path: string, data: JsonObject) => void;
5
+ }
1
6
  /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
2
- export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
7
+ export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string, hooks?: RegistryArtifactPersistHooks): void;
3
8
  //# sourceMappingURL=registry-artifact-sync.d.ts.map
@@ -1,7 +1,8 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { referenceTypeMatches } from "@deftai/directive-types";
4
- import { formatVbriefJson } from "./vbrief-json.js";
4
+ import { atomicWriteText } from "../cache/io.js";
5
+ import { formatBriefJson } from "./brief-io.js";
5
6
  import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
6
7
  function rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot) {
7
8
  if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
@@ -88,20 +89,30 @@ function registryItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot)
88
89
  return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
89
90
  }
90
91
  /** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
91
- export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
92
+ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, hooks = {}) {
92
93
  const newRel = relativeToVbrief(newPath, vbriefRoot);
93
94
  if (newRel === null) {
94
95
  return;
95
96
  }
96
- if (!existsSync(registryPath)) {
97
- return;
98
- }
99
97
  try {
100
- const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
101
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
102
- return;
98
+ let registry;
99
+ if (hooks.loadForMutation !== undefined) {
100
+ if (!existsSync(registryPath)) {
101
+ return;
102
+ }
103
+ const [loaded] = hooks.loadForMutation();
104
+ registry = loaded;
105
+ }
106
+ else {
107
+ if (!existsSync(registryPath)) {
108
+ return;
109
+ }
110
+ const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
111
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
112
+ return;
113
+ }
114
+ registry = parsed;
103
115
  }
104
- const registry = parsed;
105
116
  const plan = registry.plan;
106
117
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
107
118
  return;
@@ -149,11 +160,18 @@ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldP
149
160
  }
150
161
  }
151
162
  if (changed) {
152
- writeFileSync(registryPath, formatVbriefJson(registry), "utf8");
163
+ if (hooks.persist !== undefined) {
164
+ hooks.persist(registryPath, registry);
165
+ }
166
+ else {
167
+ atomicWriteText(registryPath, formatBriefJson(registry));
168
+ }
153
169
  }
154
170
  }
155
- catch {
156
- /* best-effort */
171
+ catch (err) {
172
+ if (hooks.loadForMutation !== undefined || hooks.persist !== undefined) {
173
+ throw err;
174
+ }
157
175
  }
158
176
  }
159
177
  //# sourceMappingURL=registry-artifact-sync.js.map
@@ -1,15 +1,16 @@
1
- import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
1
+ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
4
4
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
5
  import { hasArtifactSuffix } from "../layout/resolve.js";
6
6
  import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
7
+ import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief-io.js";
7
8
  import { stampCompletionMetadata } from "./capacity-stamp.js";
8
9
  import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
9
10
  import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
10
11
  import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
11
12
  import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
12
- import { formatVbriefJson, utcNowIso } from "./vbrief-json.js";
13
+ import { utcNowIso } from "./vbrief-json.js";
13
14
  export function runTransition(action, filePath, now = new Date()) {
14
15
  if (!(action in TRANSITIONS)) {
15
16
  const valid = Object.keys(TRANSITIONS).sort().join(", ");
@@ -43,13 +44,11 @@ export function runTransition(action, filePath, now = new Date()) {
43
44
  `File is in ${currentFolder}/.`,
44
45
  };
45
46
  }
46
- let data;
47
- try {
48
- data = JSON.parse(readFileSync(resolvedPath, "utf8"));
49
- }
50
- catch (err) {
51
- return { ok: false, message: `Invalid JSON in ${resolvedPath}: ${String(err)}` };
47
+ const readResult = readBriefForMutation(resolvedPath);
48
+ if (!readResult.ok) {
49
+ return { ok: false, message: readResult.message };
52
50
  }
51
+ const data = readResult.data;
53
52
  const plan = data.plan;
54
53
  if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
55
54
  return { ok: false, message: `Missing or invalid 'plan' object in ${resolvedPath}` };
@@ -100,7 +99,7 @@ export function runTransition(action, filePath, now = new Date()) {
100
99
  if (act === "complete") {
101
100
  stampCompletionMetadata(planObj, projectRoot, nowIso);
102
101
  }
103
- const formatted = formatVbriefJson(data);
102
+ const formatted = formatBriefJson(data);
104
103
  const crud = new InstrumentedVbriefCrud({ now: () => now });
105
104
  if (targetFolder !== null) {
106
105
  const destDir = join(vbriefRoot, targetFolder);
@@ -111,10 +110,11 @@ export function runTransition(action, filePath, now = new Date()) {
111
110
  }
112
111
  // #2578: stamp terminal status at the destination path in the same write as
113
112
  // folder placement — never leave a non-terminal status under completed/.
114
- const writeResult = crud.update(destPath, formatted, { trustedWrite: true });
113
+ const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
115
114
  if (!writeResult.ok) {
116
- return { ok: false, message: writeResult.error ?? `CRUD update failed for ${destPath}` };
115
+ return { ok: false, message: writeResult.message };
117
116
  }
117
+ crud.recordTrustedUpdate(destPath, formatted);
118
118
  try {
119
119
  unlinkSync(resolvedPath);
120
120
  }
@@ -135,18 +135,26 @@ export function runTransition(action, filePath, now = new Date()) {
135
135
  }
136
136
  updateDecomposedParentBackReferences(data, resolvedPath, destPath, vbriefRoot);
137
137
  updateDecomposedChildBackReferences(data, resolvedPath, destPath, vbriefRoot);
138
- syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
139
- syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
140
138
  const actionLabel = MOVE_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
139
+ const pdSyncError = syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
140
+ if (pdSyncError !== null) {
141
+ return {
142
+ ok: false,
143
+ message: `${actionLabel} ${basename}: brief moved to ${targetFolder}/ but ` +
144
+ `PROJECT-DEFINITION sync failed: ${pdSyncError}`,
145
+ };
146
+ }
147
+ syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
141
148
  return {
142
149
  ok: true,
143
150
  message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
144
151
  };
145
152
  }
146
- const writeResult = crud.update(resolvedPath, formatted, { trustedWrite: true });
153
+ const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
147
154
  if (!writeResult.ok) {
148
- return { ok: false, message: writeResult.error ?? `CRUD update failed for ${resolvedPath}` };
155
+ return { ok: false, message: writeResult.message };
149
156
  }
157
+ crud.recordTrustedUpdate(resolvedPath, formatted);
150
158
  try {
151
159
  persistCrudMetrics(projectRoot, crud.getMetrics());
152
160
  }
@@ -4,7 +4,7 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
4
4
  import { resolveLifecycleRoot } from "../layout/resolve.js";
5
5
  import { append, canonicalLogPath, newDecisionId, readAll } from "./audit-log.js";
6
6
  import { REVERSIBLE_ACTIONS, TERMINAL_ACTIONS } from "./constants.js";
7
- import { formatVbriefJson, utcNowIso } from "./vbrief-json.js";
7
+ import { formatBriefJson, utcNowIso } from "./vbrief-json.js";
8
8
  function vbriefRoot(projectRoot) {
9
9
  return resolveLifecycleRoot(projectRoot);
10
10
  }
@@ -185,7 +185,7 @@ function moveAndFlip(srcFile, destFolder, newStatus, timestamp, projectRoot) {
185
185
  const planObj = plan;
186
186
  planObj.status = newStatus;
187
187
  planObj.updated = timestamp;
188
- writeFileSync(srcFile, formatVbriefJson(data), "utf8");
188
+ writeFileSync(srcFile, formatBriefJson(data), "utf8");
189
189
  mkdirSync(destFolder, { recursive: true });
190
190
  const destPath = join(destFolder, basename(srcFile));
191
191
  renameSync(srcFile, destPath);
@@ -1,4 +1,3 @@
1
- /** Format vBRIEF JSON like Python json.dumps(..., indent=2, ensure_ascii=False) + newline. */
2
- export declare function formatVbriefJson(data: unknown): string;
1
+ export { formatBriefJson } from "./brief-io.js";
3
2
  export declare function utcNowIso(now?: Date): string;
4
3
  //# sourceMappingURL=vbrief-json.d.ts.map
@@ -1,7 +1,4 @@
1
- /** Format vBRIEF JSON like Python json.dumps(..., indent=2, ensure_ascii=False) + newline. */
2
- export function formatVbriefJson(data) {
3
- return `${JSON.stringify(data, null, 2)}\n`;
4
- }
1
+ export { formatBriefJson } from "./brief-io.js";
5
2
  export function utcNowIso(now = new Date()) {
6
3
  return now.toISOString().replace(/\.\d{3}Z$/, "Z");
7
4
  }
@@ -1,6 +1,11 @@
1
1
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { resolveTriageCachePath } from "../triage/cache-path.js";
3
4
  export const STATE_RELATIVE_PATH = join("xbrief", ".triage-cache", "staleness-tickler-state.json");
5
+ const STATE_FILE_NAME = "staleness-tickler-state.json";
6
+ function resolveStalenessTicklerStatePath(projectRoot) {
7
+ return resolveTriageCachePath(projectRoot, STATE_FILE_NAME);
8
+ }
4
9
  function defaultReadText(path) {
5
10
  try {
6
11
  return readFileSync(path, "utf8");
@@ -31,11 +36,23 @@ export function parseStalenessTicklerState(text) {
31
36
  return {};
32
37
  }
33
38
  export function loadStalenessTicklerState(projectRoot, io = {}) {
34
- const path = join(projectRoot, STATE_RELATIVE_PATH);
39
+ let path;
40
+ try {
41
+ path = resolveStalenessTicklerStatePath(projectRoot);
42
+ }
43
+ catch {
44
+ return {};
45
+ }
35
46
  return parseStalenessTicklerState((io.readText ?? defaultReadText)(path));
36
47
  }
37
48
  export function saveStalenessTicklerState(projectRoot, state, io = {}) {
38
- const path = join(projectRoot, STATE_RELATIVE_PATH);
49
+ let path;
50
+ try {
51
+ path = resolveStalenessTicklerStatePath(projectRoot);
52
+ }
53
+ catch {
54
+ return;
55
+ }
39
56
  try {
40
57
  (io.writeText ?? defaultWriteText)(path, `${JSON.stringify(state, null, 2)}\n`);
41
58
  }
@@ -213,6 +213,11 @@ export function loadCachedIssues(repo, options) {
213
213
  if (scanPassed === false) {
214
214
  continue;
215
215
  }
216
+ // cachePut writes content.md only when scan_result.passed is true; missing
217
+ // approved content means the entry was quarantined or tampered — fail closed.
218
+ if (scanPassed === true && !existsSync(join(entryDir, "content.md"))) {
219
+ continue;
220
+ }
216
221
  const rawTitle = typeof payload.title === "string" ? payload.title : "";
217
222
  const safeTitle = sanitizeQueueTitle(rawTitle);
218
223
  if (safeTitle === null) {
@@ -6,5 +6,6 @@ export * from "./cursor-tier1.js";
6
6
  export * from "./python-call-scan.js";
7
7
  export * from "./rule-ownership-lint.js";
8
8
  export * from "./scm-boundary.js";
9
+ export * from "./skill-external-fetch-gate.js";
9
10
  export * from "./verify-stubs.js";
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -6,5 +6,6 @@ export * from "./cursor-tier1.js";
6
6
  export * from "./python-call-scan.js";
7
7
  export * from "./rule-ownership-lint.js";
8
8
  export * from "./scm-boundary.js";
9
+ export * from "./skill-external-fetch-gate.js";
9
10
  export * from "./verify-stubs.js";
10
11
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * skill-external-fetch-gate.ts -- deterministic gate for fetch-then-execute skill prose (#1936 / #1532).
3
+ *
4
+ * Production entry for collectExternalFetchViolations: wired into
5
+ * `task verify:skill-external-fetch-gate` and `check:framework-source`.
6
+ *
7
+ * Exit codes (three-state):
8
+ * 0 -- clean: every shipped skill with external-fetch language has Security context mitigation.
9
+ * 1 -- drift: one or more skills pair fetch/follow-through with execute/install without mitigation.
10
+ * 2 -- config error: skills directory missing or unreadable.
11
+ */
12
+ export interface SkillExternalFetchGateResult {
13
+ readonly code: 0 | 1 | 2;
14
+ readonly message: string;
15
+ readonly stream: "stdout" | "stderr";
16
+ }
17
+ /**
18
+ * Evaluate the skill external-fetch gate for a framework-source or consumer tree.
19
+ */
20
+ export declare function evaluateSkillExternalFetchGate(projectRoot: string): SkillExternalFetchGateResult;
21
+ //# sourceMappingURL=skill-external-fetch-gate.d.ts.map